mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-11 10:10:25 +01:00
[PM-2857] Inline menu credit card autofill (#9848)
* [PM-2857] Inline Menu Credit Card Autofill * [PM-2857] Ensuring that we set up the fill of elements to work in an exclusive manner from other cipher types * [PM-2857] Reworking how we handle invalidating cache when a tab chagne has occurred * [PM-5189] Fixing issues found within code review behind how we position elements * [PM-5189] Fixing issues found within code review behind how we position elements * [PM-5189] Fixing issues found within code review behind how we position elements * [PM-5189] Fixing issues found within code review behind how we position elements * [PM-5189] Fixing issues found within code review behind how we position elements * [PM-5189] Fixing issues found within code review behind how we position elements * [PM-5189] Adding jest tests for OverlayBackground methods * [PM-5189] Adding jest tests for OverlayContentService methods * [PM-5189] Working through further issues on positioning of inline menu * [PM-5189] Working through further issues on positioning of inline menu * [PM-5189] Working through further issues on positioning of inline menu * [PM-5189] Working through further issues on positioning of inline menu * [PM-5189] Working through jest tests for OverlayBackground and refining repositioning delays * [PM-5189] Working through jest tests for OverlayBackground and refining repositioning delays * [PM-5189] Working through jest tests for OverlayBackground and refining repositioning delays * [PM-5189] Working through jest tests for OverlayBackground and refining repositioning delays * [PM-5189] Working through jest tests for OverlayBackground and refining repositioning delays * [PM-5189] Fixing an issue found when switching between open windows * [PM-5189] Fixing an issue found when switching between open windows * [PM-5189] Removing throttle from resize listeners within the content script * [PM-5189] Removing throttle from resize listeners within the content script * [PM-5189] Fixing issue within Safari relating to repositioning elements from outer frame * [PM-5189] Fixing issue within Safari relating to repositioning elements from outer frame * [PM-5189] Fixing issue within Safari relating to repositioning elements from outer frame * [PM-5189] Adding some documentation and adjust jest test for util method * [PM-5189] Reverting naming structure for OverlayBackground method * [PM-5189] Fixing a missed promise reference within OverlayBackground * [PM-9267] Implement Feature Flag for Inline Menu Re-Architecture * [PM-9267] Incorporating legacy OverlayBackground implementation * [PM-9267] Incorporating legacy overlay content scripts * [PM-9267] Incorporating legacy overlay content scripts * [PM-9267] Incorporating legacy overlay content scripts * [PM-9267] Incorporating legacy overlay content scripts * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-5189] Removing throttle from resize listeners within the content script * Revert "[PM-5189] Removing throttle from resize listeners within the content script" This reverts commit62cf0f8f24
. * [PM-5189] Re-adding throttle and reducing delay * [PM-2857] Fixing typing and jest issues * [PM-2857] Inline Menu Credit Card Autofill * [PM-5189] Fixing an issue with onButton click settings not being respected when a reposition event occurs * [PM-5189] Adding a missing test to OverlayBackground * [PM-2857] Incorporating logic to present the inline menu credit card UI as designed * [PM-2857] Refining card field qualification behavior * [PM-2857] Incorporating differentiated copy when opening the inline menu on a field with no ciphers present * [PM-2857] Introducing logic that handles adding a credit card from the inline menu * [PM-5189] Fixing an issue where we trigger a blur event when the inline menu is hovered, but the page takes focus away * [PM-9267] Adjusting naming convention for page files * [PM-9267] Adjusting naming convention for page files * [PM-9342] Inline menu does not show on username field for a form that has a password field with an invalid autocomplete value * [PM-2857] Introducing logic that handles adding a credit card from the inline menu * [PM-9342] Incorporating logic to handle multiple autocomplete values within a captured set of page details * [PM-9342] Incorporating logic to handle multiple autocomplete values within a captured set of page details * [PM-9342] Changing logic for how we identify new password fields to reflect a more assertive qualification * [PM-2857] Fixing an issue with how we identify ciphers in the inline menu * [PM-2857] Working through issues when adding a cipher from the inline menu for credit card ciphers * [PM-2857] Working through issues when adding a cipher from the inline menu for credit card ciphers * [PM-2857] Fixing an issue encountered with updating credit card info within the add/edit view * [PM-9342] Adding feedback from code review * [PM-5189] Fixing an issue where the port key for an inline menu element could potentially be undefined if the window focus changes too quickly * [PM-2857] Refactoring implementation for how we getCipherViews to ensure we only query card items when necessary * [PM-2857] Refactoring implementation to simplify how we create cipherViews when adding a new item * [PM-2857] Fixing an issue with how we store identity and card cipher views * [PM-2857] Fixing an issue with how we store identity and card cipher views * [PM-2857] Finalizing implementation, writing jest tests, refactoring smaller elements * [PM-2857] Finalizing implementation, writing jest tests, refactoring smaller elements * [PM-2857] Finalizing implementation, writing jest tests, refactoring smaller elements * [PM-2857] Finalizing implementation, writing jest tests, refactoring smaller elements * [PM-2857] Fixing an issue with how we store identity and card cipher views * [PM-2857] Finalizing jest tests * [PM-2857] Finalizing jest tests * [PM-2857] Adjusting an aspect of the inline menu icon * [PM-2857] Adjusting aspect of inline menu field qualification * [PM-2857] Adjusting aspect of inline menu field qualification * [PM-2857] Updating copy for unlock state to be generic * [PM-2857] Updating copy for unlock state to be generic * [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account * [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account * PM-4950 - Fix hint and verify delete components that had the data in the wrong place (#9877) * PM-4661: Add passkey.username as item.username (#9756) * Add incoming passkey.username as item.username * Driveby fix, was sending wrong username * added username to new-cipher too * Guarded the if-block * Update apps/browser/src/vault/popup/components/vault/add-edit.component.ts Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * Fixed broken test * fixed username on existing ciphers --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * PM-4878: Add passkey information to items when signing in (#9835) * Added username to subtitle * Added subName to cipher * Moved subName to component * Update apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts Co-authored-by: SmithThe4th <gsmith@bitwarden.com> * Fixed double code and added comment * Added changeDetection: ChangeDetectionStrategy.OnPush as per review --------- Co-authored-by: SmithThe4th <gsmith@bitwarden.com> * [AC-2791] Members page - finish component library refactors (#9727) * Replace PlatformUtilsService with ToastService * Remove unneeded templates * Implement table filtering function * Move member-only methods from base class to subclass * Move utility functions inside new MemberTableDataSource * Rename PeopleComponent to MembersComponent * [deps] Platform: Update angular-cli monorepo to v16.2.14 (#9380) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * [PM-8789] Move desktop_native into subcrate (#9682) * Move desktop_native into subcrate * Add publish = false to crates * [PM-6394] remove policy evaluator cache (#9807) * [PM-9364] Copy for Aggregate auto-scaling invoices for Teams and Enterprise customers (#9875) * Change the seat adjustment message * Move changes from en_GB file to en file * revert changes in en_GB file * Add feature flag to the change * use user verification as a part of key rotation (#9722) * Add the ability for custom validation logic to be injected into `UserVerificationDialogComponent` (#8770) * Introduce `verificationType` * Update template to use `verificationType` * Implement a path for `verificationType = 'custom'` * Delete `clientSideOnlyVerification` * Update `EnrollMasterPasswordResetComponent` to include a server-side hash check * Better describe the custom scenerio through comments * Add an example of the custom verficiation scenerio * Move execution of verification function into try/catch * Migrate existing uses of `clientSideOnlyVerification` * Use generic type option instead of casting * Change "given" to "determined" in a comment * Restructure the `org-redirect` guard to be Angular 17+ compliant (#9552) * Document the `org-redirect` guard in code * Make assertions about the way the `org-redirect` guard should behave * Restructure the `org-redirect` guard to be Angular 17+ compliant * Convert data parameter to function parameter * Convert a data parameter to a function parameter that was missed * Pass redirect function to default organization route * don't initialize kdf with validators, do it on first set (#9754) * add testids for attachments (#9892) * Bug fix - error toast in 2fa (#9623) * Bug fix - error toast in 2fa * Bug fix - Yubikey code obscured * 2FA error fix * [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account * [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account * [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account * [PM-2857] Fixing an issue with how we parse the last digits for credit card aria description * Restructure the `is-paid-org` guard to be Angular 17+ compliant (#9598) * Document that `is-paid-org` guard in code * Remove unused `MessagingService` dependency * Make assertions about the way the is-paid-org guard should behave * Restructure the `is-paid-org` guard to be Angular 17+ compliant * Random commit to get the build job moving * Undo previous commit * Bumped client version(s) (#9895) * [PM-9344] Clarify accepted user state (#9861) * Prefer `Needs confirmation` to `Accepted` display status This emphasizes that action is still required to complete setup. * Remove unused message * Bumped client version(s) (#9906) * Revert "Bumped client version(s) (#9906)" (#9907) This reverts commit78c2829793
. * fix duo subscriptions and org vs individual duo setup (#9859) * [PM-5024] Migrate tax-info component (#9872) * Changes for the tax info migration * Return for invalid formgroup * Restructure the `org-permissions` guard to be Angular 17+ compliant (#9631) * Document the `org-permissions` guard in code * Restructure the `org-permissions` guard to be Angular 17+ compliant * Update the `org-permissions` guard to use `ToastService` * Simplify callback function sigantures * Remove unused test object * Fix updated route from merge * Restructure the `provider-permissions` guard to be Angular 17+ compliant (#9609) * Document the `provider-permissions` guard in code * Restructure the `provider-permissions` guard to be Angular 17+ compliant * [deps] Platform: Update @types/argon2-browser to v1.18.4 (#8180) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Bumped client version(s) (#9914) * [PM-7162] Cipher Form - Item Details (#9758) * [PM-7162] Fix weird angular error regarding disabled component bit-select * [PM-7162] Introduce CipherFormConfigService and related types * [PM-7162] Introduce CipherFormService * [PM-7162] Introduce the Item Details section component and the CipherFormContainer interface * [PM-7162] Introduce the CipherForm component * [PM-7162] Add strongly typed QueryParams to the add-edit-v2.component * [PM-7162] Export CipherForm from Vault Lib * [PM-7162] Use the CipherForm in Browser AddEditV2 * [PM-7162] Introduce CipherForm storybook * [PM-7162] Remove VaultPopupListFilterService dependency from NewItemDropDownV2 component * [PM-7162] Add support for content projection of attachment button * [PM-7162] Fix typo * [PM-7162] Cipher form service cleanup * [PM-7162] Move readonly collection notice to bit-hint * [PM-7162] Refactor CipherFormConfig type to enforce required properties with Typescript * [PM-7162] Fix storybook after config changes * [PM-7162] Use new add-edit component for clone route * [deps]: Update @yao-pkg/pkg to ^5.12.0 (#9820) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Autosync the updated translations (#9922) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> * Autosync the updated translations (#9923) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> * Autosync the updated translations (#9924) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> * [AC-2830] Unable to create a free organization (#9917) * Resolve the issue free org creation * Check that the taxForm is touched * [PM-7162] Fix broken getter when original cipher is null (#9927) * [PM-8525] Edit Card (#9901) * initial add of card details section * add card number * update card brand when the card number changes * add year and month fields * add security code field * hide number and security code by default * add `id` for all form fields * update select options to match existing options * make year input numerical * only display card details for card ciphers * use style to set input height * handle numerical values for year * update heading when a brand is available * remove unused ref * use cardview types for the form * fix numerical input type * disable card details when in partial-edit mode * remove hardcoded height * update types for formBuilder * [PM-9440] Fix: handle undefined value in migration 66 (#9908) * fix: handle undefined value in migration 66 * fix: the if-statement was typo * Rename "encryptionAlgorithm" to "hashAlgorithmForEncryption" for clarity (#9891) * [PM-7972] Account switching integration with "remember email" functionality (#9750) * add account switching logic to login email service * enforce boolean and fix desktop account switcher order * [PM-9442] Add tests for undefined state values and proper emulation of ElectronStorageService in tests (#9910) * fix: handle undefined value in migration 66 * fix: the if-statement was typo * feat: duplicate error behavior in fake storage service * feat: fix all migrations that were setting undefined values * feat: add test for disabled fingrint in migration 66 * fix: default single user state saving undefined value to state * revert: awaiting floating promise gonna fix this in a separate PR * Revert "feat: fix all migrations that were setting undefined values" This reverts commit034713256c
. * feat: automatically convert save to remove * Revert "fix: default single user state saving undefined value to state" This reverts commit6c36da6ba5
. * [AC-2805] Consolidated Billing UI Updates (#9893) * Add empty state for invoices * Make cards on create client dialog tabbable * Add space in $ / month per member * Mute text, remove (Monthly) and right align menu on clients table * Made used seats account for all users and fixed column sort for used/remaining * Resize pricing cards * Rename assignedSeats to occupiedSeats * [PM-9460][deps] Tools: Update electron to v31 (#9921) * [deps] Tools: Update electron to v31 * Bump version in electron-builder --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> * [AC-1452] Restrict access to 'Organization Info' and 'Two-Step Login' settings pages with a permission check (#9483) * Guard Organization Info route - Owners only * Guard TwoFactor route - Owners only and Organization must be able to use 2FA * Update guards to use function syntax --------- Co-authored-by: Addison Beck <hello@addisonbeck.com> * [PM-9437] Use CollectionAccessDetailsResponse type now that is always the type returned from the API (#9951) * Add required env variables to desktop native build script (#9869) * [AC-2676] Remove paging logic from GroupsComponent (#9705) * remove infinite scroll, use virtual scroll instead * use TableDataSource for search * allow sorting by name * replacing PlatformUtilsService.showToast with ToastService * misc FIXMEs * [PM-9441] Catch and log exceptions during migration (#9905) * feat: catch and log exceptions during migration * Revert "feat: catch and log exceptions during migration" This reverts commitd68733b7e5
. * feat: use log service to log migration errors * Autosync the updated translations (#9972) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> * Autosync the updated translations (#9973) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> * Updated codeowners for new design system team (#9913) * Updated codeowners for new design system team. * Moved Angular and Bootstrap dependencies * Moved additional dependencies. * Updated ownership Co-authored-by: Will Martin <contact@willmartian.com> --------- Co-authored-by: Will Martin <contact@willmartian.com> * [SM-1016] Fix new access token dialog (#9918) * swap to bit-dialog title & subtitle * remove dialogRef.disableClose & use toastService * Add shared two-factor-options component (#9767) * Communicate the upcoming client vault privacy changes to MSPs (#9994) * Add a banner notification to the provider portal * Feature flag the banner * Move banner copy to messages.json * Allow for dismissing the banner * Auth/PM-7321 - Registration with Email Verification - Registration Finish Component Implementation (#9653) * PM-7321 - Temp add input password * PM-7321 - update input password based on latest PR changes to test. * PM-7321 - Progress on testing input password component + RegistrationFinishComponent checks * PM-7321 - more progress on registration finish. * PM-7321 - Wire up RegistrationFinishRequest model + AccountApiService abstraction + implementation changes for new method. * PM-7321 - WIP Registration Finish - wiring up request building and API call on submit. * PM-7321 - WIP registratin finish * PM-7321 - WIP on creating registration-finish service + web override to add org invite handling * PM-7321 - (1) Move web-registration-finish svc to web (2) Wire up exports (3) wire up RegistrationFinishComponent to call registration finish service * PM-7321 - Get CLI building * PM-7321 - Move all finish registration service and content to registration-finish feature folder. * PM-7321 - Fix RegistrationFinishService config * PM-7321 - RegistrationFinishComponent- handlePasswordFormSubmit - error handling WIP * PM-7321 - InputPasswordComp - Update to accept masterPasswordPolicyOptions as input instead of retrieving it as parent components in different scenarios will need to retrieve the policies differently (e.g., orgInvite token in registration vs direct call via org id post SSO on set password) * PM-7321 - Registration Finish - Add web specific logic for retrieving master password policies and passing them into the input password component. * PM-7321 - Registration Start - Send email via query param to registration finish page so it can create masterKey * PM-7321 - InputPassword comp - (1) Add loading input (2) Add email validation to submit logic. * PM-7321 - Registration Finish - Add submitting state and pass into input password so that the rest of the registration process keeps the child form disabled. * PM-7321 - Registration Finish - use validation service for error handling. * PM-7321 - All register routes must be dynamic and change if the feature flag changes. * PM-7321 - Test registration finish services. * PM-7321 - RegisterRouteService - Add comment documenting why the service exists. * PM-7321 - Add missing input password translations to browser & desktop * PM-7321 - WebRegistrationFinishSvc - apply PR feedback * [deps] Autofill: Update rimraf to v5.0.8 (#10008) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * [PM-9318] Fix username on protonpass import (#9889) * Fix username field used for ProtonPass import ProtonPass has changed their export format and userName is not itemEmail * Import additional field itemUsername --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> * [PM-8943] Update QRious script initialization in Authenticator two-factor provider (#9926) * create onload() for qrious as well as error messaging if QR code cannot be displayed * button and message updates and formpromise removal * load QR script async * rename and reorder methods * Delete Unused Bits of StateService (#9858) * Delete Unused Bits of StateService * Fix Tests * remove getBgService for auth request service (#10020) * [PM-2857] Fixing a jest test * [PM-9267] Implement feature flag for inline menu re-architecture (#9845) * [PM-9267] Implement Feature Flag for Inline Menu Re-Architecture * [PM-9267] Incorporating legacy OverlayBackground implementation * [PM-9267] Incorporating legacy overlay content scripts * [PM-9267] Incorporating legacy overlay content scripts * [PM-9267] Incorporating legacy overlay content scripts * [PM-9267] Incorporating legacy overlay content scripts * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Finalizing feature flag implementation * [PM-9267] Adjusting naming convention for page files * [PM-9267] Adjusting naming convention for page files * [PM-5189] Fixing an issue where we can potentially show the inline menu incorrectly after a user switches account * PM-4950 - Fix hint and verify delete components that had the data in the wrong place (#9877) * PM-4661: Add passkey.username as item.username (#9756) * Add incoming passkey.username as item.username * Driveby fix, was sending wrong username * added username to new-cipher too * Guarded the if-block * Update apps/browser/src/vault/popup/components/vault/add-edit.component.ts Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * Fixed broken test * fixed username on existing ciphers --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * PM-4878: Add passkey information to items when signing in (#9835) * Added username to subtitle * Added subName to cipher * Moved subName to component * Update apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts Co-authored-by: SmithThe4th <gsmith@bitwarden.com> * Fixed double code and added comment * Added changeDetection: ChangeDetectionStrategy.OnPush as per review --------- Co-authored-by: SmithThe4th <gsmith@bitwarden.com> * [AC-2791] Members page - finish component library refactors (#9727) * Replace PlatformUtilsService with ToastService * Remove unneeded templates * Implement table filtering function * Move member-only methods from base class to subclass * Move utility functions inside new MemberTableDataSource * Rename PeopleComponent to MembersComponent * [deps] Platform: Update angular-cli monorepo to v16.2.14 (#9380) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * [PM-8789] Move desktop_native into subcrate (#9682) * Move desktop_native into subcrate * Add publish = false to crates * [PM-6394] remove policy evaluator cache (#9807) * [PM-9364] Copy for Aggregate auto-scaling invoices for Teams and Enterprise customers (#9875) * Change the seat adjustment message * Move changes from en_GB file to en file * revert changes in en_GB file * Add feature flag to the change * use user verification as a part of key rotation (#9722) * Add the ability for custom validation logic to be injected into `UserVerificationDialogComponent` (#8770) * Introduce `verificationType` * Update template to use `verificationType` * Implement a path for `verificationType = 'custom'` * Delete `clientSideOnlyVerification` * Update `EnrollMasterPasswordResetComponent` to include a server-side hash check * Better describe the custom scenerio through comments * Add an example of the custom verficiation scenerio * Move execution of verification function into try/catch * Migrate existing uses of `clientSideOnlyVerification` * Use generic type option instead of casting * Change "given" to "determined" in a comment * Restructure the `org-redirect` guard to be Angular 17+ compliant (#9552) * Document the `org-redirect` guard in code * Make assertions about the way the `org-redirect` guard should behave * Restructure the `org-redirect` guard to be Angular 17+ compliant * Convert data parameter to function parameter * Convert a data parameter to a function parameter that was missed * Pass redirect function to default organization route * don't initialize kdf with validators, do it on first set (#9754) * add testids for attachments (#9892) * Bug fix - error toast in 2fa (#9623) * Bug fix - error toast in 2fa * Bug fix - Yubikey code obscured * 2FA error fix * Restructure the `is-paid-org` guard to be Angular 17+ compliant (#9598) * Document that `is-paid-org` guard in code * Remove unused `MessagingService` dependency * Make assertions about the way the is-paid-org guard should behave * Restructure the `is-paid-org` guard to be Angular 17+ compliant * Random commit to get the build job moving * Undo previous commit * Bumped client version(s) (#9895) * [PM-9344] Clarify accepted user state (#9861) * Prefer `Needs confirmation` to `Accepted` display status This emphasizes that action is still required to complete setup. * Remove unused message * Bumped client version(s) (#9906) * Revert "Bumped client version(s) (#9906)" (#9907) This reverts commit78c2829793
. * fix duo subscriptions and org vs individual duo setup (#9859) * [PM-5024] Migrate tax-info component (#9872) * Changes for the tax info migration * Return for invalid formgroup * Restructure the `org-permissions` guard to be Angular 17+ compliant (#9631) * Document the `org-permissions` guard in code * Restructure the `org-permissions` guard to be Angular 17+ compliant * Update the `org-permissions` guard to use `ToastService` * Simplify callback function sigantures * Remove unused test object * Fix updated route from merge * Restructure the `provider-permissions` guard to be Angular 17+ compliant (#9609) * Document the `provider-permissions` guard in code * Restructure the `provider-permissions` guard to be Angular 17+ compliant * [deps] Platform: Update @types/argon2-browser to v1.18.4 (#8180) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Bumped client version(s) (#9914) * [PM-7162] Cipher Form - Item Details (#9758) * [PM-7162] Fix weird angular error regarding disabled component bit-select * [PM-7162] Introduce CipherFormConfigService and related types * [PM-7162] Introduce CipherFormService * [PM-7162] Introduce the Item Details section component and the CipherFormContainer interface * [PM-7162] Introduce the CipherForm component * [PM-7162] Add strongly typed QueryParams to the add-edit-v2.component * [PM-7162] Export CipherForm from Vault Lib * [PM-7162] Use the CipherForm in Browser AddEditV2 * [PM-7162] Introduce CipherForm storybook * [PM-7162] Remove VaultPopupListFilterService dependency from NewItemDropDownV2 component * [PM-7162] Add support for content projection of attachment button * [PM-7162] Fix typo * [PM-7162] Cipher form service cleanup * [PM-7162] Move readonly collection notice to bit-hint * [PM-7162] Refactor CipherFormConfig type to enforce required properties with Typescript * [PM-7162] Fix storybook after config changes * [PM-7162] Use new add-edit component for clone route * [deps]: Update @yao-pkg/pkg to ^5.12.0 (#9820) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Autosync the updated translations (#9922) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> * Autosync the updated translations (#9923) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> * Autosync the updated translations (#9924) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> * [AC-2830] Unable to create a free organization (#9917) * Resolve the issue free org creation * Check that the taxForm is touched * [PM-7162] Fix broken getter when original cipher is null (#9927) * [PM-8525] Edit Card (#9901) * initial add of card details section * add card number * update card brand when the card number changes * add year and month fields * add security code field * hide number and security code by default * add `id` for all form fields * update select options to match existing options * make year input numerical * only display card details for card ciphers * use style to set input height * handle numerical values for year * update heading when a brand is available * remove unused ref * use cardview types for the form * fix numerical input type * disable card details when in partial-edit mode * remove hardcoded height * update types for formBuilder * [PM-9440] Fix: handle undefined value in migration 66 (#9908) * fix: handle undefined value in migration 66 * fix: the if-statement was typo * Rename "encryptionAlgorithm" to "hashAlgorithmForEncryption" for clarity (#9891) * [PM-7972] Account switching integration with "remember email" functionality (#9750) * add account switching logic to login email service * enforce boolean and fix desktop account switcher order * [PM-9442] Add tests for undefined state values and proper emulation of ElectronStorageService in tests (#9910) * fix: handle undefined value in migration 66 * fix: the if-statement was typo * feat: duplicate error behavior in fake storage service * feat: fix all migrations that were setting undefined values * feat: add test for disabled fingrint in migration 66 * fix: default single user state saving undefined value to state * revert: awaiting floating promise gonna fix this in a separate PR * Revert "feat: fix all migrations that were setting undefined values" This reverts commit034713256c
. * feat: automatically convert save to remove * Revert "fix: default single user state saving undefined value to state" This reverts commit6c36da6ba5
. * [AC-2805] Consolidated Billing UI Updates (#9893) * Add empty state for invoices * Make cards on create client dialog tabbable * Add space in $ / month per member * Mute text, remove (Monthly) and right align menu on clients table * Made used seats account for all users and fixed column sort for used/remaining * Resize pricing cards * Rename assignedSeats to occupiedSeats * [PM-9460][deps] Tools: Update electron to v31 (#9921) * [deps] Tools: Update electron to v31 * Bump version in electron-builder --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> * [AC-1452] Restrict access to 'Organization Info' and 'Two-Step Login' settings pages with a permission check (#9483) * Guard Organization Info route - Owners only * Guard TwoFactor route - Owners only and Organization must be able to use 2FA * Update guards to use function syntax --------- Co-authored-by: Addison Beck <hello@addisonbeck.com> * [PM-9437] Use CollectionAccessDetailsResponse type now that is always the type returned from the API (#9951) * Add required env variables to desktop native build script (#9869) * [AC-2676] Remove paging logic from GroupsComponent (#9705) * remove infinite scroll, use virtual scroll instead * use TableDataSource for search * allow sorting by name * replacing PlatformUtilsService.showToast with ToastService * misc FIXMEs * [PM-9441] Catch and log exceptions during migration (#9905) * feat: catch and log exceptions during migration * Revert "feat: catch and log exceptions during migration" This reverts commitd68733b7e5
. * feat: use log service to log migration errors * Autosync the updated translations (#9972) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> * Autosync the updated translations (#9973) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> * Updated codeowners for new design system team (#9913) * Updated codeowners for new design system team. * Moved Angular and Bootstrap dependencies * Moved additional dependencies. * Updated ownership Co-authored-by: Will Martin <contact@willmartian.com> --------- Co-authored-by: Will Martin <contact@willmartian.com> * [SM-1016] Fix new access token dialog (#9918) * swap to bit-dialog title & subtitle * remove dialogRef.disableClose & use toastService * Add shared two-factor-options component (#9767) * Communicate the upcoming client vault privacy changes to MSPs (#9994) * Add a banner notification to the provider portal * Feature flag the banner * Move banner copy to messages.json * Allow for dismissing the banner * Auth/PM-7321 - Registration with Email Verification - Registration Finish Component Implementation (#9653) * PM-7321 - Temp add input password * PM-7321 - update input password based on latest PR changes to test. * PM-7321 - Progress on testing input password component + RegistrationFinishComponent checks * PM-7321 - more progress on registration finish. * PM-7321 - Wire up RegistrationFinishRequest model + AccountApiService abstraction + implementation changes for new method. * PM-7321 - WIP Registration Finish - wiring up request building and API call on submit. * PM-7321 - WIP registratin finish * PM-7321 - WIP on creating registration-finish service + web override to add org invite handling * PM-7321 - (1) Move web-registration-finish svc to web (2) Wire up exports (3) wire up RegistrationFinishComponent to call registration finish service * PM-7321 - Get CLI building * PM-7321 - Move all finish registration service and content to registration-finish feature folder. * PM-7321 - Fix RegistrationFinishService config * PM-7321 - RegistrationFinishComponent- handlePasswordFormSubmit - error handling WIP * PM-7321 - InputPasswordComp - Update to accept masterPasswordPolicyOptions as input instead of retrieving it as parent components in different scenarios will need to retrieve the policies differently (e.g., orgInvite token in registration vs direct call via org id post SSO on set password) * PM-7321 - Registration Finish - Add web specific logic for retrieving master password policies and passing them into the input password component. * PM-7321 - Registration Start - Send email via query param to registration finish page so it can create masterKey * PM-7321 - InputPassword comp - (1) Add loading input (2) Add email validation to submit logic. * PM-7321 - Registration Finish - Add submitting state and pass into input password so that the rest of the registration process keeps the child form disabled. * PM-7321 - Registration Finish - use validation service for error handling. * PM-7321 - All register routes must be dynamic and change if the feature flag changes. * PM-7321 - Test registration finish services. * PM-7321 - RegisterRouteService - Add comment documenting why the service exists. * PM-7321 - Add missing input password translations to browser & desktop * PM-7321 - WebRegistrationFinishSvc - apply PR feedback * [deps] Autofill: Update rimraf to v5.0.8 (#10008) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * [PM-9318] Fix username on protonpass import (#9889) * Fix username field used for ProtonPass import ProtonPass has changed their export format and userName is not itemEmail * Import additional field itemUsername --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> * [PM-8943] Update QRious script initialization in Authenticator two-factor provider (#9926) * create onload() for qrious as well as error messaging if QR code cannot be displayed * button and message updates and formpromise removal * load QR script async * rename and reorder methods * Delete Unused Bits of StateService (#9858) * Delete Unused Bits of StateService * Fix Tests * remove getBgService for auth request service (#10020) --------- Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Co-authored-by: Anders Åberg <anders@andersaberg.com> Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: SmithThe4th <gsmith@bitwarden.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com> Co-authored-by: ✨ Audrey ✨ <ajensen@bitwarden.com> Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Co-authored-by: Jake Fink <jfink@bitwarden.com> Co-authored-by: Addison Beck <github@addisonbeck.com> Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Co-authored-by: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Co-authored-by: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Co-authored-by: Matt Gibson <mgibson@bitwarden.com> Co-authored-by: Opeyemi <Alaoopeyemi101@gmail.com> Co-authored-by: Shane Melton <smelton@bitwarden.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> Co-authored-by: Bernd Schoolmann <mail@quexten.com> Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> Co-authored-by: Addison Beck <hello@addisonbeck.com> Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com> Co-authored-by: Will Martin <contact@willmartian.com> Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com> --------- Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Co-authored-by: Anders Åberg <anders@andersaberg.com> Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: SmithThe4th <gsmith@bitwarden.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com> Co-authored-by: ✨ Audrey ✨ <ajensen@bitwarden.com> Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Co-authored-by: Jake Fink <jfink@bitwarden.com> Co-authored-by: Addison Beck <github@addisonbeck.com> Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Co-authored-by: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Co-authored-by: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Co-authored-by: Matt Gibson <mgibson@bitwarden.com> Co-authored-by: Opeyemi <Alaoopeyemi101@gmail.com> Co-authored-by: Shane Melton <smelton@bitwarden.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> Co-authored-by: Bernd Schoolmann <mail@quexten.com> Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> Co-authored-by: Addison Beck <hello@addisonbeck.com> Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com> Co-authored-by: Will Martin <contact@willmartian.com> Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com>
This commit is contained in:
parent
bb5f56838a
commit
a950f2242e
@ -3046,6 +3046,10 @@
|
|||||||
"message": "Unlock your account to view matching logins",
|
"message": "Unlock your account to view matching logins",
|
||||||
"description": "Text to display in overlay when the account is locked."
|
"description": "Text to display in overlay when the account is locked."
|
||||||
},
|
},
|
||||||
|
"unlockYourAccountToViewAutofillSuggestions": {
|
||||||
|
"message": "Unlock your account to view autofill suggestions",
|
||||||
|
"description": "Text to display in overlay when the account is locked."
|
||||||
|
},
|
||||||
"unlockAccount": {
|
"unlockAccount": {
|
||||||
"message": "Unlock account",
|
"message": "Unlock account",
|
||||||
"description": "Button text to display in overlay when the account is locked."
|
"description": "Button text to display in overlay when the account is locked."
|
||||||
@ -3070,6 +3074,22 @@
|
|||||||
"message": "Add new vault item",
|
"message": "Add new vault item",
|
||||||
"description": "Screen reader text (aria-label) for new item button in overlay"
|
"description": "Screen reader text (aria-label) for new item button in overlay"
|
||||||
},
|
},
|
||||||
|
"newLogin": {
|
||||||
|
"message": "New login",
|
||||||
|
"description": "Button text to display within inline menu when there are no matching items on a login field"
|
||||||
|
},
|
||||||
|
"addNewLoginItem": {
|
||||||
|
"message": "Add new vault login item",
|
||||||
|
"description": "Screen reader text (aria-label) for new login button within inline menu"
|
||||||
|
},
|
||||||
|
"newCard": {
|
||||||
|
"message": "New card",
|
||||||
|
"description": "Button text to display within inline menu when there are no matching items on a credit card field"
|
||||||
|
},
|
||||||
|
"addNewCardItem": {
|
||||||
|
"message": "Add new vault card item",
|
||||||
|
"description": "Screen reader text (aria-label) for new card button within inline menu"
|
||||||
|
},
|
||||||
"bitwardenOverlayMenuAvailable": {
|
"bitwardenOverlayMenuAvailable": {
|
||||||
"message": "Bitwarden auto-fill menu available. Press the down arrow key to select.",
|
"message": "Bitwarden auto-fill menu available. Press the down arrow key to select.",
|
||||||
"description": "Screen reader text for announcing when the overlay opens on the page"
|
"description": "Screen reader text for announcing when the overlay opens on the page"
|
||||||
@ -3743,6 +3763,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cardNumberEndsWith": {
|
||||||
|
"message": "card number ends with",
|
||||||
|
"description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher."
|
||||||
|
},
|
||||||
"loginCredentials": {
|
"loginCredentials": {
|
||||||
"message": "Login credentials"
|
"message": "Login credentials"
|
||||||
},
|
},
|
||||||
|
@ -34,6 +34,7 @@ export type WebsiteIconData = {
|
|||||||
export type FocusedFieldData = {
|
export type FocusedFieldData = {
|
||||||
focusedFieldStyles: Partial<CSSStyleDeclaration>;
|
focusedFieldStyles: Partial<CSSStyleDeclaration>;
|
||||||
focusedFieldRects: Partial<DOMRect>;
|
focusedFieldRects: Partial<DOMRect>;
|
||||||
|
filledByCipherType?: CipherType;
|
||||||
tabId?: number;
|
tabId?: number;
|
||||||
frameId?: number;
|
frameId?: number;
|
||||||
};
|
};
|
||||||
@ -50,13 +51,26 @@ export type InlineMenuPosition = {
|
|||||||
list?: InlineMenuElementPosition;
|
list?: InlineMenuElementPosition;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OverlayAddNewItemMessage = {
|
export type NewLoginCipherData = {
|
||||||
login?: {
|
|
||||||
uri?: string;
|
uri?: string;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type NewCardCipherData = {
|
||||||
|
cardholderName: string;
|
||||||
|
number: string;
|
||||||
|
expirationMonth: string;
|
||||||
|
expirationYear: string;
|
||||||
|
expirationDate?: string;
|
||||||
|
cvv: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OverlayAddNewItemMessage = {
|
||||||
|
addNewCipherType?: CipherType;
|
||||||
|
login?: NewLoginCipherData;
|
||||||
|
card?: NewCardCipherData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CloseInlineMenuMessage = {
|
export type CloseInlineMenuMessage = {
|
||||||
@ -91,6 +105,7 @@ export type OverlayPortMessage = {
|
|||||||
command: string;
|
command: string;
|
||||||
direction?: string;
|
direction?: string;
|
||||||
inlineMenuCipherId?: string;
|
inlineMenuCipherId?: string;
|
||||||
|
addNewCipherType?: CipherType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InlineMenuCipherData = {
|
export type InlineMenuCipherData = {
|
||||||
@ -178,7 +193,7 @@ export type InlineMenuListPortMessageHandlers = {
|
|||||||
autofillInlineMenuBlurred: () => void;
|
autofillInlineMenuBlurred: () => void;
|
||||||
unlockVault: ({ port }: PortConnectionParam) => void;
|
unlockVault: ({ port }: PortConnectionParam) => void;
|
||||||
fillAutofillInlineMenuCipher: ({ message, port }: PortOnMessageHandlerParams) => void;
|
fillAutofillInlineMenuCipher: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||||
addNewVaultItem: ({ port }: PortConnectionParam) => void;
|
addNewVaultItem: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||||
viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void;
|
viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||||
redirectAutofillInlineMenuFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void;
|
redirectAutofillInlineMenuFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||||
updateAutofillInlineMenuListHeight: ({ message, port }: PortOnMessageHandlerParams) => void;
|
updateAutofillInlineMenuListHeight: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||||
@ -187,5 +202,5 @@ export type InlineMenuListPortMessageHandlers = {
|
|||||||
export interface OverlayBackground {
|
export interface OverlayBackground {
|
||||||
init(): Promise<void>;
|
init(): Promise<void>;
|
||||||
removePageDetails(tabId: number): void;
|
removePageDetails(tabId: number): void;
|
||||||
updateOverlayCiphers(): Promise<void>;
|
updateOverlayCiphers(updateAllCipherTypes?: boolean): Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -322,6 +322,7 @@ describe("OverlayBackground", () => {
|
|||||||
it("removes the page details and port key for a specific tab from the pageDetailsForTab object", async () => {
|
it("removes the page details and port key for a specific tab from the pageDetailsForTab object", async () => {
|
||||||
await initOverlayElementPorts();
|
await initOverlayElementPorts();
|
||||||
const tabId = 1;
|
const tabId = 1;
|
||||||
|
portKeyForTabSpy[tabId] = "portKey";
|
||||||
sendMockExtensionMessage(
|
sendMockExtensionMessage(
|
||||||
{ command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() },
|
{ command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() },
|
||||||
mock<chrome.runtime.MessageSender>({ tab: createChromeTabMock({ id: tabId }), frameId: 1 }),
|
mock<chrome.runtime.MessageSender>({ tab: createChromeTabMock({ id: tabId }), frameId: 1 }),
|
||||||
@ -705,6 +706,13 @@ describe("OverlayBackground", () => {
|
|||||||
type: CipherType.Card,
|
type: CipherType.Card,
|
||||||
card: { subTitle: "subtitle-2" },
|
card: { subTitle: "subtitle-2" },
|
||||||
});
|
});
|
||||||
|
const cipher3 = mock<CipherView>({
|
||||||
|
id: "id-3",
|
||||||
|
localData: { lastUsedDate: 222 },
|
||||||
|
name: "name-3",
|
||||||
|
type: CipherType.Login,
|
||||||
|
login: { username: "username-3", uri: url },
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||||
@ -751,16 +759,53 @@ describe("OverlayBackground", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("queries all ciphers for the given url, sort them by last used, and format them for usage in the overlay", async () => {
|
it("queries all cipher types, sorts them by last used, and formats them for usage in the overlay", async () => {
|
||||||
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
|
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
|
||||||
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
|
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
|
||||||
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
|
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
|
||||||
|
|
||||||
await overlayBackground.updateOverlayCiphers();
|
await overlayBackground.updateOverlayCiphers();
|
||||||
|
|
||||||
|
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
|
||||||
|
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [CipherType.Card]);
|
||||||
|
expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled();
|
||||||
|
expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual(
|
||||||
|
new Map([
|
||||||
|
["inline-menu-cipher-0", cipher2],
|
||||||
|
["inline-menu-cipher-1", cipher1],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("queries only login ciphers when not updating all cipher types", async () => {
|
||||||
|
overlayBackground["cardAndIdentityCiphers"] = new Set([]);
|
||||||
|
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
|
||||||
|
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher3, cipher1]);
|
||||||
|
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
|
||||||
|
|
||||||
|
await overlayBackground.updateOverlayCiphers(false);
|
||||||
|
|
||||||
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
|
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
|
||||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url);
|
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url);
|
||||||
expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled();
|
expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled();
|
||||||
|
expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual(
|
||||||
|
new Map([
|
||||||
|
["inline-menu-cipher-0", cipher1],
|
||||||
|
["inline-menu-cipher-1", cipher3],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("queries all cipher types when the card and identity ciphers set is not built when only updating login ciphers", async () => {
|
||||||
|
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
|
||||||
|
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
|
||||||
|
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
|
||||||
|
|
||||||
|
await overlayBackground.updateOverlayCiphers(false);
|
||||||
|
|
||||||
|
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
|
||||||
|
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [CipherType.Card]);
|
||||||
|
expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled();
|
||||||
expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual(
|
expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual(
|
||||||
new Map([
|
new Map([
|
||||||
["inline-menu-cipher-0", cipher2],
|
["inline-menu-cipher-0", cipher2],
|
||||||
@ -771,6 +816,7 @@ describe("OverlayBackground", () => {
|
|||||||
|
|
||||||
it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateAutofillInlineMenuListCiphers` message to the tab indicating that the list of ciphers is populated", async () => {
|
it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateAutofillInlineMenuListCiphers` message to the tab indicating that the list of ciphers is populated", async () => {
|
||||||
overlayBackground["inlineMenuListPort"] = mock<chrome.runtime.Port>();
|
overlayBackground["inlineMenuListPort"] = mock<chrome.runtime.Port>();
|
||||||
|
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id });
|
||||||
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
|
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
|
||||||
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
|
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
|
||||||
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
|
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
|
||||||
@ -780,21 +826,6 @@ describe("OverlayBackground", () => {
|
|||||||
expect(overlayBackground["inlineMenuListPort"].postMessage).toHaveBeenCalledWith({
|
expect(overlayBackground["inlineMenuListPort"].postMessage).toHaveBeenCalledWith({
|
||||||
command: "updateAutofillInlineMenuListCiphers",
|
command: "updateAutofillInlineMenuListCiphers",
|
||||||
ciphers: [
|
ciphers: [
|
||||||
{
|
|
||||||
card: cipher2.card.subTitle,
|
|
||||||
favorite: cipher2.favorite,
|
|
||||||
icon: {
|
|
||||||
fallbackImage: "",
|
|
||||||
icon: "bwi-credit-card",
|
|
||||||
image: undefined,
|
|
||||||
imageEnabled: true,
|
|
||||||
},
|
|
||||||
id: "inline-menu-cipher-0",
|
|
||||||
login: null,
|
|
||||||
name: "name-2",
|
|
||||||
reprompt: cipher2.reprompt,
|
|
||||||
type: 3,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
card: null,
|
card: null,
|
||||||
favorite: cipher1.favorite,
|
favorite: cipher1.favorite,
|
||||||
@ -810,7 +841,7 @@ describe("OverlayBackground", () => {
|
|||||||
},
|
},
|
||||||
name: "name-1",
|
name: "name-1",
|
||||||
reprompt: cipher1.reprompt,
|
reprompt: cipher1.reprompt,
|
||||||
type: 1,
|
type: CipherType.Login,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -884,6 +915,7 @@ describe("OverlayBackground", () => {
|
|||||||
sendMockExtensionMessage(
|
sendMockExtensionMessage(
|
||||||
{
|
{
|
||||||
command: "autofillOverlayAddNewVaultItem",
|
command: "autofillOverlayAddNewVaultItem",
|
||||||
|
addNewCipherType: CipherType.Login,
|
||||||
login: {
|
login: {
|
||||||
uri: "https://tacos.com",
|
uri: "https://tacos.com",
|
||||||
hostname: "",
|
hostname: "",
|
||||||
@ -899,6 +931,29 @@ describe("OverlayBackground", () => {
|
|||||||
expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher");
|
expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher");
|
||||||
expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled();
|
expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("creates a new card cipher", async () => {
|
||||||
|
sendMockExtensionMessage(
|
||||||
|
{
|
||||||
|
command: "autofillOverlayAddNewVaultItem",
|
||||||
|
addNewCipherType: CipherType.Card,
|
||||||
|
card: {
|
||||||
|
cardholderName: "cardholderName",
|
||||||
|
number: "4242424242424242",
|
||||||
|
expirationMonth: "12",
|
||||||
|
expirationYear: "2025",
|
||||||
|
expirationDate: "12/25",
|
||||||
|
cvv: "123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sender,
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
|
||||||
|
expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher");
|
||||||
|
expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("checkIsInlineMenuCiphersPopulated message handler", () => {
|
describe("checkIsInlineMenuCiphersPopulated message handler", () => {
|
||||||
@ -929,8 +984,9 @@ describe("OverlayBackground", () => {
|
|||||||
|
|
||||||
it("returns true if the overlay login ciphers are populated", async () => {
|
it("returns true if the overlay login ciphers are populated", async () => {
|
||||||
overlayBackground["inlineMenuCiphers"] = new Map([
|
overlayBackground["inlineMenuCiphers"] = new Map([
|
||||||
["inline-menu-cipher-0", mock<CipherView>()],
|
["inline-menu-cipher-0", mock<CipherView>({ type: CipherType.Login })],
|
||||||
]);
|
]);
|
||||||
|
await overlayBackground["getInlineMenuCipherData"]();
|
||||||
|
|
||||||
sendMockExtensionMessage(
|
sendMockExtensionMessage(
|
||||||
{ command: "checkIsInlineMenuCiphersPopulated" },
|
{ command: "checkIsInlineMenuCiphersPopulated" },
|
||||||
@ -2029,12 +2085,16 @@ describe("OverlayBackground", () => {
|
|||||||
sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender);
|
sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
sendPortMessage(listMessageConnectorSpy, { command: "addNewVaultItem", portKey });
|
sendPortMessage(listMessageConnectorSpy, {
|
||||||
|
command: "addNewVaultItem",
|
||||||
|
portKey,
|
||||||
|
addNewCipherType: CipherType.Login,
|
||||||
|
});
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(tabsSendMessageSpy).toHaveBeenCalledWith(
|
expect(tabsSendMessageSpy).toHaveBeenCalledWith(
|
||||||
sender.tab,
|
sender.tab,
|
||||||
{ command: "addNewVaultItemFromOverlay" },
|
{ command: "addNewVaultItemFromOverlay", addNewCipherType: CipherType.Login },
|
||||||
{ frameId: sender.frameId },
|
{ frameId: sender.frameId },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -19,6 +19,7 @@ import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-stat
|
|||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
|
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
|
||||||
|
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||||
@ -54,6 +55,8 @@ import {
|
|||||||
CloseInlineMenuMessage,
|
CloseInlineMenuMessage,
|
||||||
InlineMenuPosition,
|
InlineMenuPosition,
|
||||||
ToggleInlineMenuHiddenMessage,
|
ToggleInlineMenuHiddenMessage,
|
||||||
|
NewLoginCipherData,
|
||||||
|
NewCardCipherData,
|
||||||
} from "./abstractions/overlay.background";
|
} from "./abstractions/overlay.background";
|
||||||
|
|
||||||
export class OverlayBackground implements OverlayBackgroundInterface {
|
export class OverlayBackground implements OverlayBackgroundInterface {
|
||||||
@ -69,6 +72,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
private inlineMenuCiphers: Map<string, CipherView> = new Map();
|
private inlineMenuCiphers: Map<string, CipherView> = new Map();
|
||||||
private inlineMenuPageTranslations: Record<string, string>;
|
private inlineMenuPageTranslations: Record<string, string>;
|
||||||
private inlineMenuPosition: InlineMenuPosition = {};
|
private inlineMenuPosition: InlineMenuPosition = {};
|
||||||
|
private cardAndIdentityCiphers: Set<CipherView> | null = null;
|
||||||
|
private currentInlineMenuCiphersCount: number = 0;
|
||||||
private delayedCloseTimeout: number | NodeJS.Timeout;
|
private delayedCloseTimeout: number | NodeJS.Timeout;
|
||||||
private startInlineMenuFadeInSubject = new Subject<void>();
|
private startInlineMenuFadeInSubject = new Subject<void>();
|
||||||
private cancelInlineMenuFadeInSubject = new Subject<boolean>();
|
private cancelInlineMenuFadeInSubject = new Subject<boolean>();
|
||||||
@ -132,7 +137,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
autofillInlineMenuBlurred: () => this.checkInlineMenuButtonFocused(),
|
autofillInlineMenuBlurred: () => this.checkInlineMenuButtonFocused(),
|
||||||
unlockVault: ({ port }) => this.unlockVault(port),
|
unlockVault: ({ port }) => this.unlockVault(port),
|
||||||
fillAutofillInlineMenuCipher: ({ message, port }) => this.fillInlineMenuCipher(message, port),
|
fillAutofillInlineMenuCipher: ({ message, port }) => this.fillInlineMenuCipher(message, port),
|
||||||
addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port),
|
addNewVaultItem: ({ message, port }) => this.getNewVaultItemDetails(message, port),
|
||||||
viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port),
|
viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port),
|
||||||
redirectAutofillInlineMenuFocusOut: ({ message, port }) =>
|
redirectAutofillInlineMenuFocusOut: ({ message, port }) =>
|
||||||
this.redirectInlineMenuFocusOut(message, port),
|
this.redirectInlineMenuFocusOut(message, port),
|
||||||
@ -220,7 +225,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
* Queries all ciphers for the given url, and sorts them by last used. Will not update the
|
* Queries all ciphers for the given url, and sorts them by last used. Will not update the
|
||||||
* list of ciphers if the extension is not unlocked.
|
* list of ciphers if the extension is not unlocked.
|
||||||
*/
|
*/
|
||||||
async updateOverlayCiphers() {
|
async updateOverlayCiphers(updateAllCipherTypes = true) {
|
||||||
const authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
|
const authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||||
if (authStatus !== AuthenticationStatus.Unlocked) {
|
if (authStatus !== AuthenticationStatus.Unlocked) {
|
||||||
if (this.focusedFieldData) {
|
if (this.focusedFieldData) {
|
||||||
@ -235,9 +240,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.inlineMenuCiphers = new Map();
|
this.inlineMenuCiphers = new Map();
|
||||||
const ciphersViews = (
|
const ciphersViews = await this.getCipherViews(currentTab, updateAllCipherTypes);
|
||||||
await this.cipherService.getAllDecryptedForUrl(currentTab?.url || "")
|
|
||||||
).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
|
|
||||||
for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) {
|
for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) {
|
||||||
this.inlineMenuCiphers.set(`inline-menu-cipher-${cipherIndex}`, ciphersViews[cipherIndex]);
|
this.inlineMenuCiphers.set(`inline-menu-cipher-${cipherIndex}`, ciphersViews[cipherIndex]);
|
||||||
}
|
}
|
||||||
@ -249,6 +252,51 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the decrypted ciphers within a user's vault based on the current tab's URL.
|
||||||
|
*
|
||||||
|
* @param currentTab - The current tab
|
||||||
|
* @param updateAllCipherTypes - Identifies credit card and identity cipher types should also be updated
|
||||||
|
*/
|
||||||
|
private async getCipherViews(
|
||||||
|
currentTab: chrome.tabs.Tab,
|
||||||
|
updateAllCipherTypes: boolean,
|
||||||
|
): Promise<CipherView[]> {
|
||||||
|
if (updateAllCipherTypes || !this.cardAndIdentityCiphers) {
|
||||||
|
return this.getAllCipherTypeViews(currentTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipherViews = (
|
||||||
|
await this.cipherService.getAllDecryptedForUrl(currentTab?.url || "")
|
||||||
|
).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
|
||||||
|
|
||||||
|
return cipherViews.concat(...this.cardAndIdentityCiphers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries all cipher types from the user's vault returns them sorted by last used.
|
||||||
|
*
|
||||||
|
* @param currentTab - The current tab
|
||||||
|
*/
|
||||||
|
private async getAllCipherTypeViews(currentTab: chrome.tabs.Tab): Promise<CipherView[]> {
|
||||||
|
if (!this.cardAndIdentityCiphers) {
|
||||||
|
this.cardAndIdentityCiphers = new Set([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cardAndIdentityCiphers.clear();
|
||||||
|
const cipherViews = (
|
||||||
|
await this.cipherService.getAllDecryptedForUrl(currentTab.url, [CipherType.Card])
|
||||||
|
).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
|
||||||
|
for (let cipherIndex = 0; cipherIndex < cipherViews.length; cipherIndex++) {
|
||||||
|
const cipherView = cipherViews[cipherIndex];
|
||||||
|
if (cipherView.type === CipherType.Card && !this.cardAndIdentityCiphers.has(cipherView)) {
|
||||||
|
this.cardAndIdentityCiphers.add(cipherView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cipherViews;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strips out unnecessary data from the ciphers and returns an array of
|
* Strips out unnecessary data from the ciphers and returns an array of
|
||||||
* objects that contain the cipher data needed for the inline menu list.
|
* objects that contain the cipher data needed for the inline menu list.
|
||||||
@ -260,6 +308,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
|
|
||||||
for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) {
|
for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) {
|
||||||
const [inlineMenuCipherId, cipher] = inlineMenuCiphersArray[cipherIndex];
|
const [inlineMenuCipherId, cipher] = inlineMenuCiphersArray[cipherIndex];
|
||||||
|
if (this.focusedFieldData?.filledByCipherType !== cipher.type) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
inlineMenuCipherData.push({
|
inlineMenuCipherData.push({
|
||||||
id: inlineMenuCipherId,
|
id: inlineMenuCipherId,
|
||||||
@ -273,6 +324,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.currentInlineMenuCiphersCount = inlineMenuCipherData.length;
|
||||||
return inlineMenuCipherData;
|
return inlineMenuCipherData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1062,7 +1114,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
buttonPageTitle: this.i18nService.translate("bitwardenOverlayButton"),
|
buttonPageTitle: this.i18nService.translate("bitwardenOverlayButton"),
|
||||||
toggleBitwardenVaultOverlay: this.i18nService.translate("toggleBitwardenVaultOverlay"),
|
toggleBitwardenVaultOverlay: this.i18nService.translate("toggleBitwardenVaultOverlay"),
|
||||||
listPageTitle: this.i18nService.translate("bitwardenVault"),
|
listPageTitle: this.i18nService.translate("bitwardenVault"),
|
||||||
unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewMatchingLogins"),
|
unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewAutofillSuggestions"),
|
||||||
unlockAccount: this.i18nService.translate("unlockAccount"),
|
unlockAccount: this.i18nService.translate("unlockAccount"),
|
||||||
fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"),
|
fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"),
|
||||||
username: this.i18nService.translate("username")?.toLowerCase(),
|
username: this.i18nService.translate("username")?.toLowerCase(),
|
||||||
@ -1070,6 +1122,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
noItemsToShow: this.i18nService.translate("noItemsToShow"),
|
noItemsToShow: this.i18nService.translate("noItemsToShow"),
|
||||||
newItem: this.i18nService.translate("newItem"),
|
newItem: this.i18nService.translate("newItem"),
|
||||||
addNewVaultItem: this.i18nService.translate("addNewVaultItem"),
|
addNewVaultItem: this.i18nService.translate("addNewVaultItem"),
|
||||||
|
newLogin: this.i18nService.translate("newLogin"),
|
||||||
|
addNewLoginItem: this.i18nService.translate("addNewLoginItem"),
|
||||||
|
newCard: this.i18nService.translate("newCard"),
|
||||||
|
addNewCardItem: this.i18nService.translate("addNewCardItem"),
|
||||||
|
cardNumberEndsWith: this.i18nService.translate("cardNumberEndsWith"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1100,16 +1157,20 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
* Triggers adding a new vault item from the overlay. Gathers data
|
* Triggers adding a new vault item from the overlay. Gathers data
|
||||||
* input by the user before calling to open the add/edit window.
|
* input by the user before calling to open the add/edit window.
|
||||||
*
|
*
|
||||||
|
* @param addNewCipherType - The type of cipher to add
|
||||||
* @param sender - The sender of the port message
|
* @param sender - The sender of the port message
|
||||||
*/
|
*/
|
||||||
private getNewVaultItemDetails({ sender }: chrome.runtime.Port) {
|
private getNewVaultItemDetails(
|
||||||
if (!this.senderTabHasFocusedField(sender)) {
|
{ addNewCipherType }: OverlayPortMessage,
|
||||||
|
{ sender }: chrome.runtime.Port,
|
||||||
|
) {
|
||||||
|
if (!addNewCipherType || !this.senderTabHasFocusedField(sender)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BrowserApi.tabSendMessage(
|
void BrowserApi.tabSendMessage(
|
||||||
sender.tab,
|
sender.tab,
|
||||||
{ command: "addNewVaultItemFromOverlay" },
|
{ command: "addNewVaultItemFromOverlay", addNewCipherType },
|
||||||
{
|
{
|
||||||
frameId: this.focusedFieldData.frameId || 0,
|
frameId: this.focusedFieldData.frameId || 0,
|
||||||
},
|
},
|
||||||
@ -1120,18 +1181,60 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
* Handles adding a new vault item from the overlay. Gathers data login
|
* Handles adding a new vault item from the overlay. Gathers data login
|
||||||
* data captured in the extension message.
|
* data captured in the extension message.
|
||||||
*
|
*
|
||||||
|
* @param addNewCipherType - The type of cipher to add
|
||||||
* @param login - The login data captured from the extension message
|
* @param login - The login data captured from the extension message
|
||||||
|
* @param card - The card data captured from the extension message
|
||||||
* @param sender - The sender of the extension message
|
* @param sender - The sender of the extension message
|
||||||
*/
|
*/
|
||||||
private async addNewVaultItem(
|
private async addNewVaultItem(
|
||||||
{ login }: OverlayAddNewItemMessage,
|
{ addNewCipherType, login, card }: OverlayAddNewItemMessage,
|
||||||
sender: chrome.runtime.MessageSender,
|
sender: chrome.runtime.MessageSender,
|
||||||
) {
|
) {
|
||||||
if (!login) {
|
if (!addNewCipherType) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cipherView: CipherView = this.buildNewVaultItemCipherView({
|
||||||
|
addNewCipherType,
|
||||||
|
login,
|
||||||
|
card,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cipherView) {
|
||||||
this.closeInlineMenu(sender);
|
this.closeInlineMenu(sender);
|
||||||
|
await this.cipherService.setAddEditCipherInfo({
|
||||||
|
cipher: cipherView,
|
||||||
|
collectionIds: cipherView.collectionIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
|
||||||
|
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and returns a new cipher view with the provided vault item data.
|
||||||
|
*
|
||||||
|
* @param addNewCipherType - The type of cipher to add
|
||||||
|
* @param login - The login data captured from the extension message
|
||||||
|
* @param card - The card data captured from the extension message
|
||||||
|
*/
|
||||||
|
private buildNewVaultItemCipherView({ addNewCipherType, login, card }: OverlayAddNewItemMessage) {
|
||||||
|
if (login && addNewCipherType === CipherType.Login) {
|
||||||
|
return this.buildLoginCipherView(login);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (card && addNewCipherType === CipherType.Card) {
|
||||||
|
return this.buildCardCipherView(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a new login cipher view with the provided login data.
|
||||||
|
*
|
||||||
|
* @param login - The login data captured from the extension message
|
||||||
|
*/
|
||||||
|
private buildLoginCipherView(login: NewLoginCipherData) {
|
||||||
const uriView = new LoginUriView();
|
const uriView = new LoginUriView();
|
||||||
uriView.uri = login.uri;
|
uriView.uri = login.uri;
|
||||||
|
|
||||||
@ -1146,13 +1249,30 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
cipherView.type = CipherType.Login;
|
cipherView.type = CipherType.Login;
|
||||||
cipherView.login = loginView;
|
cipherView.login = loginView;
|
||||||
|
|
||||||
await this.cipherService.setAddEditCipherInfo({
|
return cipherView;
|
||||||
cipher: cipherView,
|
}
|
||||||
collectionIds: cipherView.collectionIds,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
|
/**
|
||||||
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
|
* Builds a new card cipher view with the provided card data.
|
||||||
|
*
|
||||||
|
* @param card - The card data captured from the extension message
|
||||||
|
*/
|
||||||
|
private buildCardCipherView(card: NewCardCipherData) {
|
||||||
|
const cardView = new CardView();
|
||||||
|
cardView.cardholderName = card.cardholderName || "";
|
||||||
|
cardView.number = card.number || "";
|
||||||
|
cardView.expMonth = card.expirationMonth || "";
|
||||||
|
cardView.expYear = card.expirationYear || "";
|
||||||
|
cardView.code = card.cvv || "";
|
||||||
|
cardView.brand = card.number ? CardView.getCardBrandByPatterns(card.number) : "";
|
||||||
|
|
||||||
|
const cipherView = new CipherView();
|
||||||
|
cipherView.name = "";
|
||||||
|
cipherView.folderId = null;
|
||||||
|
cipherView.type = CipherType.Card;
|
||||||
|
cipherView.card = cardView;
|
||||||
|
|
||||||
|
return cipherView;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1209,7 +1329,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
* @param sender - The sender of the message
|
* @param sender - The sender of the message
|
||||||
*/
|
*/
|
||||||
private checkIsInlineMenuCiphersPopulated(sender: chrome.runtime.MessageSender) {
|
private checkIsInlineMenuCiphersPopulated(sender: chrome.runtime.MessageSender) {
|
||||||
return this.senderTabHasFocusedField(sender) && this.inlineMenuCiphers.size > 0;
|
return this.senderTabHasFocusedField(sender) && this.currentInlineMenuCiphersCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1477,6 +1597,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
portName: isInlineMenuListPort
|
portName: isInlineMenuListPort
|
||||||
? AutofillOverlayPort.ListMessageConnector
|
? AutofillOverlayPort.ListMessageConnector
|
||||||
: AutofillOverlayPort.ButtonMessageConnector,
|
: AutofillOverlayPort.ButtonMessageConnector,
|
||||||
|
filledByCipherType: this.focusedFieldData?.filledByCipherType,
|
||||||
});
|
});
|
||||||
void this.updateInlineMenuPosition(
|
void this.updateInlineMenuPosition(
|
||||||
{
|
{
|
||||||
|
@ -104,7 +104,7 @@ export default class TabsBackground {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.overlayBackground.updateOverlayCiphers();
|
await this.overlayBackground.updateOverlayCiphers(false);
|
||||||
|
|
||||||
if (this.main.onUpdatedRan) {
|
if (this.main.onUpdatedRan) {
|
||||||
return;
|
return;
|
||||||
@ -134,7 +134,7 @@ export default class TabsBackground {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.main.refreshBadge(),
|
this.main.refreshBadge(),
|
||||||
this.main.refreshMenu(),
|
this.main.refreshMenu(),
|
||||||
this.overlayBackground.updateOverlayCiphers(),
|
this.overlayBackground.updateOverlayCiphers(false),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
import { AutofillOverlayElementType } from "../../enums/autofill-overlay.enum";
|
import { AutofillOverlayElementType } from "../../enums/autofill-overlay.enum";
|
||||||
import AutofillScript from "../../models/autofill-script";
|
import AutofillScript from "../../models/autofill-script";
|
||||||
@ -18,6 +19,7 @@ export type AutofillExtensionMessage = {
|
|||||||
isFocusingFieldElement?: boolean;
|
isFocusingFieldElement?: boolean;
|
||||||
authStatus?: AuthenticationStatus;
|
authStatus?: AuthenticationStatus;
|
||||||
isOpeningFullInlineMenu?: boolean;
|
isOpeningFullInlineMenu?: boolean;
|
||||||
|
addNewCipherType?: CipherType;
|
||||||
data?: {
|
data?: {
|
||||||
direction?: "previous" | "next" | "current";
|
direction?: "previous" | "next" | "current";
|
||||||
forceCloseInlineMenu?: boolean;
|
forceCloseInlineMenu?: boolean;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a single field that is collected from the page source and is potentially autofilled.
|
* Represents a single field that is collected from the page source and is potentially autofilled.
|
||||||
*/
|
*/
|
||||||
@ -106,4 +108,6 @@ export default class AutofillField {
|
|||||||
rel?: string | null;
|
rel?: string | null;
|
||||||
|
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
|
|
||||||
|
filledByCipherType?: CipherType;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
import { InlineMenuCipherData } from "../../../background/abstractions/overlay.background";
|
import { InlineMenuCipherData } from "../../../background/abstractions/overlay.background";
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ export type InitAutofillInlineMenuListMessage = AutofillInlineMenuListMessage &
|
|||||||
theme: string;
|
theme: string;
|
||||||
translations: Record<string, string>;
|
translations: Record<string, string>;
|
||||||
ciphers?: InlineMenuCipherData[];
|
ciphers?: InlineMenuCipherData[];
|
||||||
|
filledByCipherType?: CipherType;
|
||||||
portKey: string;
|
portKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with an empty list of ciphers creates the views for the no results inline menu 1`] = `
|
exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with an empty list of ciphers creates the views for the no results inline menu that does not have a fill by cipher type 1`] = `
|
||||||
<div
|
<div
|
||||||
class="inline-menu-list-container theme_light"
|
class="inline-menu-list-container theme_light"
|
||||||
>
|
>
|
||||||
@ -13,7 +13,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
|
|||||||
class="inline-menu-list-button-container"
|
class="inline-menu-list-button-container"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="addNewVaultItem, opensInANewWindow"
|
aria-label=", opensInANewWindow"
|
||||||
class="add-new-item-button inline-menu-list-button"
|
class="add-new-item-button inline-menu-list-button"
|
||||||
id="new-item-button"
|
id="new-item-button"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@ -45,13 +45,112 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
|
|||||||
</clippath>
|
</clippath>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
newItem
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers for an authenticated user creates the view for a list of ciphers 1`] = `
|
exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with an empty list of ciphers creates the views for the no results inline menu that should be filled by a card cipher 1`] = `
|
||||||
|
<div
|
||||||
|
class="inline-menu-list-container theme_light"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="no-items inline-menu-list-message"
|
||||||
|
>
|
||||||
|
noItemsToShow
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="inline-menu-list-button-container"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label=", opensInANewWindow"
|
||||||
|
class="add-new-item-button inline-menu-list-button"
|
||||||
|
id="new-item-button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="17"
|
||||||
|
viewBox="0 0 16 17"
|
||||||
|
width="16"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
clip-path="url(#a)"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M15.222 7.914H8.963a.471.471 0 0 1-.34-.147.512.512 0 0 1-.142-.353V.99c0-.133-.05-.26-.14-.354a.471.471 0 0 0-.68 0 .51.51 0 0 0-.142.354v6.424c0 .132-.051.26-.142.353a.474.474 0 0 1-.34.147H.777a.47.47 0 0 0-.34.146.5.5 0 0 0-.14.354.522.522 0 0 0 .14.353.48.48 0 0 0 .34.147h6.26c.128 0 .25.052.34.146.09.094.142.221.142.354v6.576c0 .132.05.26.14.353a.471.471 0 0 0 .68 0 .512.512 0 0 0 .142-.353V9.414c0-.133.051-.26.142-.354a.474.474 0 0 1 .34-.146h6.26c.127 0 .25-.053.34-.147a.511.511 0 0 0 0-.707.472.472 0 0 0-.34-.146Z"
|
||||||
|
fill="#175DDC"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clippath
|
||||||
|
id="a"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 .49h16v16H0z"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
</clippath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with an empty list of ciphers creates the views for the no results inline menu that should be filled by a login cipher 1`] = `
|
||||||
|
<div
|
||||||
|
class="inline-menu-list-container theme_light"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="no-items inline-menu-list-message"
|
||||||
|
>
|
||||||
|
noItemsToShow
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="inline-menu-list-button-container"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label=", opensInANewWindow"
|
||||||
|
class="add-new-item-button inline-menu-list-button"
|
||||||
|
id="new-item-button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="17"
|
||||||
|
viewBox="0 0 16 17"
|
||||||
|
width="16"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
clip-path="url(#a)"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M15.222 7.914H8.963a.471.471 0 0 1-.34-.147.512.512 0 0 1-.142-.353V.99c0-.133-.05-.26-.14-.354a.471.471 0 0 0-.68 0 .51.51 0 0 0-.142.354v6.424c0 .132-.051.26-.142.353a.474.474 0 0 1-.34.147H.777a.47.47 0 0 0-.34.146.5.5 0 0 0-.14.354.522.522 0 0 0 .14.353.48.48 0 0 0 .34.147h6.26c.128 0 .25.052.34.146.09.094.142.221.142.354v6.576c0 .132.05.26.14.353a.471.471 0 0 0 .68 0 .512.512 0 0 0 .142-.353V9.414c0-.133.051-.26.142-.354a.474.474 0 0 1 .34-.146h6.26c.127 0 .25-.053.34-.147a.511.511 0 0 0 0-.707.472.472 0 0 0-.34-.146Z"
|
||||||
|
fill="#175DDC"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clippath
|
||||||
|
id="a"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 .49h16v16H0z"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
</clippath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers for an authenticated user creates the view for a list of login ciphers 1`] = `
|
||||||
<div
|
<div
|
||||||
class="inline-menu-list-container theme_light"
|
class="inline-menu-list-container theme_light"
|
||||||
>
|
>
|
||||||
@ -87,7 +186,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
|
|||||||
website login 1
|
website login 1
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="cipher-user-login"
|
class="cipher-subtitle"
|
||||||
title="username1"
|
title="username1"
|
||||||
>
|
>
|
||||||
username1
|
username1
|
||||||
@ -156,7 +255,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
|
|||||||
website login 2
|
website login 2
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="cipher-user-login"
|
class="cipher-subtitle"
|
||||||
title="username2"
|
title="username2"
|
||||||
>
|
>
|
||||||
username2
|
username2
|
||||||
@ -297,7 +396,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
|
|||||||
website login 4
|
website login 4
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="cipher-user-login"
|
class="cipher-subtitle"
|
||||||
title="username4"
|
title="username4"
|
||||||
>
|
>
|
||||||
username4
|
username4
|
||||||
@ -367,7 +466,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
|
|||||||
website login 5
|
website login 5
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="cipher-user-login"
|
class="cipher-subtitle"
|
||||||
title="username5"
|
title="username5"
|
||||||
>
|
>
|
||||||
username5
|
username5
|
||||||
@ -437,7 +536,439 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
|
|||||||
website login 6
|
website login 6
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="cipher-user-login"
|
class="cipher-subtitle"
|
||||||
|
title="username6"
|
||||||
|
>
|
||||||
|
username6
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="view website login 6, opensInANewWindow"
|
||||||
|
class="view-cipher-button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
clip-path="url(#a)"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||||
|
fill="#175DDC"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clippath
|
||||||
|
id="a"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 .113h20v19.773H0z"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
</clippath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers for an authenticated user creates the views for a list of card ciphers 1`] = `
|
||||||
|
<div
|
||||||
|
class="inline-menu-list-container theme_light"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
class="inline-menu-list-actions"
|
||||||
|
role="list"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="inline-menu-list-actions-item"
|
||||||
|
role="listitem"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="cipher-container"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-description="username, username1"
|
||||||
|
aria-label="fillCredentialsFor website login 1"
|
||||||
|
class="fill-cipher-button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="cipher-icon"
|
||||||
|
style="background-image: url(https://jest-testing-website.com/image.png);"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="cipher-details"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="cipher-name"
|
||||||
|
title="website login 1"
|
||||||
|
>
|
||||||
|
website login 1
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="cipher-subtitle"
|
||||||
|
title="username1"
|
||||||
|
>
|
||||||
|
username1
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="view website login 1, opensInANewWindow"
|
||||||
|
class="view-cipher-button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
clip-path="url(#a)"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||||
|
fill="#175DDC"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clippath
|
||||||
|
id="a"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 .113h20v19.773H0z"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
</clippath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="inline-menu-list-actions-item"
|
||||||
|
role="listitem"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="cipher-container"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-description="username, username2"
|
||||||
|
aria-label="fillCredentialsFor website login 2"
|
||||||
|
class="fill-cipher-button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="cipher-icon bwi bw-icon"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="cipher-details"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="cipher-name"
|
||||||
|
title="website login 2"
|
||||||
|
>
|
||||||
|
website login 2
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="cipher-subtitle"
|
||||||
|
title="username2"
|
||||||
|
>
|
||||||
|
username2
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="view website login 2, opensInANewWindow"
|
||||||
|
class="view-cipher-button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
clip-path="url(#a)"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||||
|
fill="#175DDC"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clippath
|
||||||
|
id="a"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 .113h20v19.773H0z"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
</clippath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="inline-menu-list-actions-item"
|
||||||
|
role="listitem"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="cipher-container"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-description="username, "
|
||||||
|
aria-label="fillCredentialsFor "
|
||||||
|
class="fill-cipher-button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="cipher-icon bwi bw-icon"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="cipher-details"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="view , opensInANewWindow"
|
||||||
|
class="view-cipher-button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
clip-path="url(#a)"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||||
|
fill="#175DDC"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clippath
|
||||||
|
id="a"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 .113h20v19.773H0z"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
</clippath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="inline-menu-list-actions-item"
|
||||||
|
role="listitem"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="cipher-container"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-description="username, username4"
|
||||||
|
aria-label="fillCredentialsFor website login 4"
|
||||||
|
class="fill-cipher-button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="cipher-icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="25"
|
||||||
|
viewBox="0 0 24 25"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M18.026 17.842c-1.418 1.739-3.517 2.84-5.86 2.84a7.364 7.364 0 0 1-3.431-.848l.062-.15.062-.151.063-.157c.081-.203.17-.426.275-.646.133-.28.275-.522.426-.68.026-.028.101-.075.275-.115.165-.037.376-.059.629-.073.138-.008.288-.014.447-.02.399-.016.847-.034 1.266-.092.314-.044.566-.131.755-.271a.884.884 0 0 0 .352-.555c.037-.2.008-.392-.03-.543-.035-.135-.084-.264-.12-.355l-.01-.03a4.26 4.26 0 0 0-.145-.33c-.126-.264-.237-.497-.288-1.085-.03-.344.09-.73.251-1.138l.089-.22c.05-.123.1-.247.14-.355.064-.171.129-.375.129-.566a1.51 1.51 0 0 0-.134-.569 2.573 2.573 0 0 0-.319-.547c-.246-.323-.635-.669-1.093-.669-.44 0-1.006.169-1.487.368-.246.102-.48.216-.68.33-.192.111-.372.235-.492.359-.93.96-1.48 1.239-1.81 1.258-.277.017-.478-.15-.736-.525a9.738 9.738 0 0 1-.19-.29l-.006-.01a11.568 11.568 0 0 0-.198-.305 2.76 2.76 0 0 0-.521-.6 1.39 1.39 0 0 0-1.088-.314 8.302 8.302 0 0 1 1.987-3.936c.055.342.146.626.272.856.23.42.561.64.926.716.406.086.857-.061 1.26-.216.125-.047.248-.097.372-.147.309-.125.618-.25.947-.341.26-.072.581-.057.959.012.264.049.529.118.8.19l.36.091c.379.094.782.178 1.135.148.374-.032.733-.197.934-.623a.874.874 0 0 0 .024-.752c-.087-.197-.24-.355-.35-.47-.26-.267-.412-.427-.412-.685 0-.125.037-.2.09-.263a.982.982 0 0 1 .303-.211c.059-.03.119-.058.183-.089l.036-.016a3.79 3.79 0 0 0 .236-.118c.047-.026.098-.056.148-.093 1.936.747 3.51 2.287 4.368 4.249a7.739 7.739 0 0 0-.031-.004c-.38-.047-.738-.056-1.063.061-.34.123-.603.368-.817.74-.122.211-.284.43-.463.67l-.095.129c-.207.281-.431.595-.58.92-.15.326-.245.705-.142 1.103.104.397.387.738.837 1.036.099.065.225.112.314.145l.02.008c.108.04.195.074.268.117.07.042.106.08.124.114.017.03.037.087.022.206-.047.376-.069.73-.052 1.034.017.292.071.59.218.809.118.174.12.421.108.786v.01a2.46 2.46 0 0 0 .021.518.809.809 0 0 0 .15.35Zm1.357.059a9.654 9.654 0 0 0 1.62-5.386c0-5.155-3.957-9.334-8.836-9.334-4.88 0-8.836 4.179-8.836 9.334 0 3.495 1.82 6.543 4.513 8.142v.093h.161a8.426 8.426 0 0 0 4.162 1.098c2.953 0 5.568-1.53 7.172-3.882a.569.569 0 0 0 .048-.062l-.004-.003ZM8.152 19.495a43.345 43.345 0 0 1 .098-.238l.057-.142c.082-.205.182-.455.297-.698.143-.301.323-.624.552-.864.163-.172.392-.254.602-.302.219-.05.473-.073.732-.088.162-.01.328-.016.495-.023.386-.015.782-.03 1.168-.084.255-.036.392-.099.461-.15.06-.045.076-.084.083-.12a.534.534 0 0 0-.02-.223 2.552 2.552 0 0 0-.095-.278l-.01-.027a3.128 3.128 0 0 0-.104-.232c-.134-.282-.31-.65-.374-1.381-.046-.533.138-1.063.3-1.472.035-.09.069-.172.1-.249.046-.11.086-.21.123-.31.062-.169.083-.264.083-.312a.812.812 0 0 0-.076-.283 1.867 1.867 0 0 0-.23-.394c-.21-.274-.428-.408-.577-.408-.315 0-.788.13-1.246.32a5.292 5.292 0 0 0-.603.293 1.727 1.727 0 0 0-.347.244c-.936.968-1.641 1.421-2.235 1.457-.646.04-1.036-.413-1.31-.813-.07-.103-.139-.21-.203-.311l-.005-.007c-.064-.101-.125-.197-.188-.29a2.098 2.098 0 0 0-.387-.453.748.748 0 0 0-.436-.18c-.1-.006-.22.005-.365.046a8.707 8.707 0 0 0-.056.992c0 2.957 1.488 5.547 3.716 6.98Zm10.362-2.316.003-.192.002-.046c.01-.305.026-.786-.232-1.169-.036-.054-.082-.189-.096-.444-.014-.243.003-.55.047-.9a1.051 1.051 0 0 0-.105-.649.987.987 0 0 0-.374-.374 2.285 2.285 0 0 0-.367-.166h-.003a1.243 1.243 0 0 1-.205-.088c-.369-.244-.505-.46-.549-.629-.044-.168-.015-.364.099-.61.115-.25.297-.511.507-.796l.089-.12c.178-.239.368-.495.512-.745.152-.263.302-.382.466-.441.18-.065.416-.073.77-.03.142.018.275.04.397.063.274.837.423 1.736.423 2.671a8.45 8.45 0 0 1-1.384 4.665Zm-4.632-12.63a7.362 7.362 0 0 0-1.715-.201c-1.89 0-3.621.716-4.965 1.905.025.54.12.887.24 1.105.13.238.295.34.482.38.2.042.484-.027.905-.188l.328-.13c.32-.13.681-.275 1.048-.377.398-.111.833-.075 1.24 0 .289.053.59.132.871.205l.326.084c.383.094.694.151.932.13.216-.017.326-.092.395-.237.039-.083.027-.114.014-.144-.027-.062-.088-.136-.212-.264l-.043-.044c-.218-.222-.567-.578-.567-1.142 0-.305.101-.547.262-.734.137-.159.308-.267.46-.347Z"
|
||||||
|
fill="#777"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="cipher-details"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="cipher-name"
|
||||||
|
title="website login 4"
|
||||||
|
>
|
||||||
|
website login 4
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="cipher-subtitle"
|
||||||
|
title="username4"
|
||||||
|
>
|
||||||
|
username4
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="view website login 4, opensInANewWindow"
|
||||||
|
class="view-cipher-button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
clip-path="url(#a)"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||||
|
fill="#175DDC"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clippath
|
||||||
|
id="a"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 .113h20v19.773H0z"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
</clippath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="inline-menu-list-actions-item"
|
||||||
|
role="listitem"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="cipher-container"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-description="username, username5"
|
||||||
|
aria-label="fillCredentialsFor website login 5"
|
||||||
|
class="fill-cipher-button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="cipher-icon"
|
||||||
|
style="background-image: url(https://jest-testing-website.com/image.png);"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="cipher-details"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="cipher-name"
|
||||||
|
title="website login 5"
|
||||||
|
>
|
||||||
|
website login 5
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="cipher-subtitle"
|
||||||
|
title="username5"
|
||||||
|
>
|
||||||
|
username5
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="view website login 5, opensInANewWindow"
|
||||||
|
class="view-cipher-button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
clip-path="url(#a)"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||||
|
fill="#175DDC"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clippath
|
||||||
|
id="a"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 .113h20v19.773H0z"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
</clippath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="inline-menu-list-actions-item"
|
||||||
|
role="listitem"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="cipher-container"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-description="username, username6"
|
||||||
|
aria-label="fillCredentialsFor website login 6"
|
||||||
|
class="fill-cipher-button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="cipher-icon"
|
||||||
|
style="background-image: url(https://jest-testing-website.com/image.png);"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="cipher-details"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="cipher-name"
|
||||||
|
title="website login 6"
|
||||||
|
>
|
||||||
|
website login 6
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="cipher-subtitle"
|
||||||
title="username6"
|
title="username6"
|
||||||
>
|
>
|
||||||
username6
|
username6
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
import { createInitAutofillInlineMenuListMessageMock } from "../../../../spec/autofill-mocks";
|
import {
|
||||||
|
createAutofillOverlayCipherDataMock,
|
||||||
|
createInitAutofillInlineMenuListMessageMock,
|
||||||
|
} from "../../../../spec/autofill-mocks";
|
||||||
import { flushPromises, postWindowMessage } from "../../../../spec/testing-utils";
|
import { flushPromises, postWindowMessage } from "../../../../spec/testing-utils";
|
||||||
|
|
||||||
import { AutofillInlineMenuList } from "./autofill-inline-menu-list";
|
import { AutofillInlineMenuList } from "./autofill-inline-menu-list";
|
||||||
@ -69,7 +73,33 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates the views for the no results inline menu", () => {
|
it("creates the views for the no results inline menu that should be filled by a login cipher", () => {
|
||||||
|
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates the views for the no results inline menu that should be filled by a card cipher", () => {
|
||||||
|
postWindowMessage(
|
||||||
|
createInitAutofillInlineMenuListMessageMock({
|
||||||
|
authStatus: AuthenticationStatus.Unlocked,
|
||||||
|
ciphers: [],
|
||||||
|
filledByCipherType: CipherType.Card,
|
||||||
|
portKey,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates the views for the no results inline menu that does not have a fill by cipher type", () => {
|
||||||
|
postWindowMessage(
|
||||||
|
createInitAutofillInlineMenuListMessageMock({
|
||||||
|
authStatus: AuthenticationStatus.Unlocked,
|
||||||
|
ciphers: [],
|
||||||
|
filledByCipherType: undefined,
|
||||||
|
portKey,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
|
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -80,7 +110,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
addVaultItemButton.dispatchEvent(new Event("click"));
|
addVaultItemButton.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "addNewVaultItem", portKey },
|
{ command: "addNewVaultItem", portKey, addNewCipherType: CipherType.Login },
|
||||||
"*",
|
"*",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -91,7 +121,37 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates the view for a list of ciphers", () => {
|
it("creates the view for a list of login ciphers", () => {
|
||||||
|
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates the views for a list of card ciphers", () => {
|
||||||
|
postWindowMessage(
|
||||||
|
createInitAutofillInlineMenuListMessageMock({
|
||||||
|
filledByCipherType: CipherType.Card,
|
||||||
|
ciphers: [
|
||||||
|
createAutofillOverlayCipherDataMock(1, {
|
||||||
|
type: CipherType.Card,
|
||||||
|
card: "Visa, *4234",
|
||||||
|
login: null,
|
||||||
|
icon: {
|
||||||
|
imageEnabled: true,
|
||||||
|
icon: "bw-id-card card-visa",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
createAutofillOverlayCipherDataMock(1, {
|
||||||
|
type: CipherType.Card,
|
||||||
|
card: "*2234",
|
||||||
|
login: null,
|
||||||
|
icon: {
|
||||||
|
imageEnabled: true,
|
||||||
|
icon: "bw-id-card card-visa",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
|
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,13 +2,14 @@ import "@webcomponents/custom-elements";
|
|||||||
import "lit/polyfill-support.js";
|
import "lit/polyfill-support.js";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background";
|
import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background";
|
||||||
import { buildSvgDomElement } from "../../../../utils";
|
import { buildSvgDomElement } from "../../../../utils";
|
||||||
import { globeIcon, lockIcon, plusIcon, viewCipherIcon } from "../../../../utils/svg-icons";
|
import { globeIcon, lockIcon, plusIcon, viewCipherIcon } from "../../../../utils/svg-icons";
|
||||||
import {
|
import {
|
||||||
InitAutofillInlineMenuListMessage,
|
|
||||||
AutofillInlineMenuListWindowMessageHandlers,
|
AutofillInlineMenuListWindowMessageHandlers,
|
||||||
|
InitAutofillInlineMenuListMessage,
|
||||||
} from "../../abstractions/autofill-inline-menu-list";
|
} from "../../abstractions/autofill-inline-menu-list";
|
||||||
import { AutofillInlineMenuPageElement } from "../shared/autofill-inline-menu-page-element";
|
import { AutofillInlineMenuPageElement } from "../shared/autofill-inline-menu-page-element";
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
private cipherListScrollIsDebounced = false;
|
private cipherListScrollIsDebounced = false;
|
||||||
private cipherListScrollDebounceTimeout: number | NodeJS.Timeout;
|
private cipherListScrollDebounceTimeout: number | NodeJS.Timeout;
|
||||||
private currentCipherIndex = 0;
|
private currentCipherIndex = 0;
|
||||||
|
private filledByCipherType: CipherType;
|
||||||
private readonly showCiphersPerPage = 6;
|
private readonly showCiphersPerPage = 6;
|
||||||
private readonly inlineMenuListWindowMessageHandlers: AutofillInlineMenuListWindowMessageHandlers =
|
private readonly inlineMenuListWindowMessageHandlers: AutofillInlineMenuListWindowMessageHandlers =
|
||||||
{
|
{
|
||||||
@ -46,6 +48,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
* @param authStatus - The current authentication status.
|
* @param authStatus - The current authentication status.
|
||||||
* @param ciphers - The ciphers to display in the inline menu list.
|
* @param ciphers - The ciphers to display in the inline menu list.
|
||||||
* @param portKey - Background generated key that allows the port to communicate with the background.
|
* @param portKey - Background generated key that allows the port to communicate with the background.
|
||||||
|
* @param filledByCipherType - The type of cipher that fills the current field.
|
||||||
*/
|
*/
|
||||||
private async initAutofillInlineMenuList({
|
private async initAutofillInlineMenuList({
|
||||||
translations,
|
translations,
|
||||||
@ -54,6 +57,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
authStatus,
|
authStatus,
|
||||||
ciphers,
|
ciphers,
|
||||||
portKey,
|
portKey,
|
||||||
|
filledByCipherType,
|
||||||
}: InitAutofillInlineMenuListMessage) {
|
}: InitAutofillInlineMenuListMessage) {
|
||||||
const linkElement = await this.initAutofillInlineMenuPage(
|
const linkElement = await this.initAutofillInlineMenuPage(
|
||||||
"list",
|
"list",
|
||||||
@ -62,6 +66,8 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
portKey,
|
portKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.filledByCipherType = filledByCipherType;
|
||||||
|
|
||||||
const themeClass = `theme_${theme}`;
|
const themeClass = `theme_${theme}`;
|
||||||
globalThis.document.documentElement.classList.add(themeClass);
|
globalThis.document.documentElement.classList.add(themeClass);
|
||||||
|
|
||||||
@ -157,10 +163,10 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
newItemButton.tabIndex = -1;
|
newItemButton.tabIndex = -1;
|
||||||
newItemButton.id = "new-item-button";
|
newItemButton.id = "new-item-button";
|
||||||
newItemButton.classList.add("add-new-item-button", "inline-menu-list-button");
|
newItemButton.classList.add("add-new-item-button", "inline-menu-list-button");
|
||||||
newItemButton.textContent = this.getTranslation("newItem");
|
newItemButton.textContent = this.getNewItemButtonText();
|
||||||
newItemButton.setAttribute(
|
newItemButton.setAttribute(
|
||||||
"aria-label",
|
"aria-label",
|
||||||
`${this.getTranslation("addNewVaultItem")}, ${this.getTranslation("opensInANewWindow")}`,
|
`${this.getNewItemAriaLabel()}, ${this.getTranslation("opensInANewWindow")}`,
|
||||||
);
|
);
|
||||||
newItemButton.prepend(buildSvgDomElement(plusIcon));
|
newItemButton.prepend(buildSvgDomElement(plusIcon));
|
||||||
newItemButton.addEventListener(EVENTS.CLICK, this.handeNewItemButtonClick);
|
newItemButton.addEventListener(EVENTS.CLICK, this.handeNewItemButtonClick);
|
||||||
@ -172,12 +178,45 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
this.inlineMenuListContainer.append(noItemsMessage, inlineMenuListButtonContainer);
|
this.inlineMenuListContainer.append(noItemsMessage, inlineMenuListButtonContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the new item text for the button based on the cipher type the focused field is filled by.
|
||||||
|
*/
|
||||||
|
private getNewItemButtonText() {
|
||||||
|
if (this.filledByCipherType === CipherType.Login) {
|
||||||
|
return this.getTranslation("newLogin");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.filledByCipherType === CipherType.Card) {
|
||||||
|
return this.getTranslation("newCard");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getTranslation("newItem");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the aria label for the new item button based on the cipher type the focused field is filled by.
|
||||||
|
*/
|
||||||
|
private getNewItemAriaLabel() {
|
||||||
|
if (this.filledByCipherType === CipherType.Login) {
|
||||||
|
return this.getTranslation("addNewLoginItem");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.filledByCipherType === CipherType.Card) {
|
||||||
|
return this.getTranslation("addNewCardItem");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getTranslation("addNewVaultItem");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the click event for the new item button.
|
* Handles the click event for the new item button.
|
||||||
* Sends a message to the parent window to add a new vault item.
|
* Sends a message to the parent window to add a new vault item.
|
||||||
*/
|
*/
|
||||||
private handeNewItemButtonClick = () => {
|
private handeNewItemButtonClick = () => {
|
||||||
this.postMessageToParent({ command: "addNewVaultItem" });
|
this.postMessageToParent({
|
||||||
|
command: "addNewVaultItem",
|
||||||
|
addNewCipherType: this.filledByCipherType,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -267,10 +306,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
"aria-label",
|
"aria-label",
|
||||||
`${this.getTranslation("fillCredentialsFor")} ${cipher.name}`,
|
`${this.getTranslation("fillCredentialsFor")} ${cipher.name}`,
|
||||||
);
|
);
|
||||||
fillCipherElement.setAttribute(
|
this.addFillCipherElementAriaDescription(fillCipherElement, cipher);
|
||||||
"aria-description",
|
|
||||||
`${this.getTranslation("username")}, ${cipher.login.username}`,
|
|
||||||
);
|
|
||||||
fillCipherElement.append(cipherIcon, cipherDetailsElement);
|
fillCipherElement.append(cipherIcon, cipherDetailsElement);
|
||||||
fillCipherElement.addEventListener(EVENTS.CLICK, this.handleFillCipherClickEvent(cipher));
|
fillCipherElement.addEventListener(EVENTS.CLICK, this.handleFillCipherClickEvent(cipher));
|
||||||
fillCipherElement.addEventListener(EVENTS.KEYUP, this.handleFillCipherKeyUpEvent);
|
fillCipherElement.addEventListener(EVENTS.KEYUP, this.handleFillCipherKeyUpEvent);
|
||||||
@ -278,6 +314,44 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
return fillCipherElement;
|
return fillCipherElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an aria description to the fill cipher button for a given cipher.
|
||||||
|
*
|
||||||
|
* @param fillCipherElement - The fill cipher button element.
|
||||||
|
* @param cipher - The cipher to add the aria description for.
|
||||||
|
*/
|
||||||
|
private addFillCipherElementAriaDescription(
|
||||||
|
fillCipherElement: HTMLButtonElement,
|
||||||
|
cipher: InlineMenuCipherData,
|
||||||
|
) {
|
||||||
|
if (cipher.login) {
|
||||||
|
fillCipherElement.setAttribute(
|
||||||
|
"aria-description",
|
||||||
|
`${this.getTranslation("username")}, ${cipher.login.username}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cipher.card) {
|
||||||
|
const cardParts = cipher.card.split(", *");
|
||||||
|
if (cardParts.length === 1) {
|
||||||
|
const cardDigits = cardParts[0].startsWith("*") ? cardParts[0].substring(1) : cardParts[0];
|
||||||
|
fillCipherElement.setAttribute(
|
||||||
|
"aria-description",
|
||||||
|
`${this.getTranslation("cardNumberEndsWith")} ${cardDigits}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardBrand = cardParts[0];
|
||||||
|
const cardDigits = cardParts[1];
|
||||||
|
fillCipherElement.setAttribute(
|
||||||
|
"aria-description",
|
||||||
|
`${cardBrand}, ${this.getTranslation("cardNumberEndsWith")} ${cardDigits}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the click event for the fill cipher button.
|
* Handles the click event for the fill cipher button.
|
||||||
* Sends a message to the parent window to fill the selected cipher.
|
* Sends a message to the parent window to fill the selected cipher.
|
||||||
@ -412,7 +486,8 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
dummyImageElement.src = url.href;
|
dummyImageElement.src = url.href;
|
||||||
dummyImageElement.addEventListener("error", () => {
|
dummyImageElement.addEventListener("error", () => {
|
||||||
cipherIcon.style.backgroundImage = "";
|
cipherIcon.style.backgroundImage = "";
|
||||||
cipherIcon.classList.add("cipher-icon", "bwi", cipher.icon.icon);
|
const iconClasses = cipher.icon.icon.split(" ");
|
||||||
|
cipherIcon.classList.add("cipher-icon", "bwi", ...iconClasses);
|
||||||
});
|
});
|
||||||
dummyImageElement.remove();
|
dummyImageElement.remove();
|
||||||
|
|
||||||
@ -423,7 +498,8 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cipher.icon?.icon) {
|
if (cipher.icon?.icon) {
|
||||||
cipherIcon.classList.add("cipher-icon", "bwi", cipher.icon.icon);
|
const iconClasses = cipher.icon.icon.split(" ");
|
||||||
|
cipherIcon.classList.add("cipher-icon", "bwi", ...iconClasses);
|
||||||
return cipherIcon;
|
return cipherIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,21 +508,21 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the details for a given cipher. Includes the cipher name and username login.
|
* Builds the details for a given cipher. Includes the cipher name and subtitle.
|
||||||
*
|
*
|
||||||
* @param cipher - The cipher to build the details for.
|
* @param cipher - The cipher to build the details for.
|
||||||
*/
|
*/
|
||||||
private buildCipherDetailsElement(cipher: InlineMenuCipherData) {
|
private buildCipherDetailsElement(cipher: InlineMenuCipherData) {
|
||||||
const cipherNameElement = this.buildCipherNameElement(cipher);
|
const cipherNameElement = this.buildCipherNameElement(cipher);
|
||||||
const cipherUserLoginElement = this.buildCipherUserLoginElement(cipher);
|
const cipherSubtitleElement = this.buildCipherSubtitleElement(cipher);
|
||||||
|
|
||||||
const cipherDetailsElement = globalThis.document.createElement("span");
|
const cipherDetailsElement = globalThis.document.createElement("span");
|
||||||
cipherDetailsElement.classList.add("cipher-details");
|
cipherDetailsElement.classList.add("cipher-details");
|
||||||
if (cipherNameElement) {
|
if (cipherNameElement) {
|
||||||
cipherDetailsElement.appendChild(cipherNameElement);
|
cipherDetailsElement.appendChild(cipherNameElement);
|
||||||
}
|
}
|
||||||
if (cipherUserLoginElement) {
|
if (cipherSubtitleElement) {
|
||||||
cipherDetailsElement.appendChild(cipherUserLoginElement);
|
cipherDetailsElement.appendChild(cipherSubtitleElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cipherDetailsElement;
|
return cipherDetailsElement;
|
||||||
@ -471,21 +547,22 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the username login element for a given cipher.
|
* Builds the subtitle element for a given cipher.
|
||||||
*
|
*
|
||||||
* @param cipher - The cipher to build the username login element for.
|
* @param cipher - The cipher to build the username login element for.
|
||||||
*/
|
*/
|
||||||
private buildCipherUserLoginElement(cipher: InlineMenuCipherData): HTMLSpanElement | null {
|
private buildCipherSubtitleElement(cipher: InlineMenuCipherData): HTMLSpanElement | null {
|
||||||
if (!cipher.login?.username) {
|
const subTitleText = cipher.login?.username || cipher.card;
|
||||||
|
if (!subTitleText) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cipherUserLoginElement = globalThis.document.createElement("span");
|
const cipherSubtitleElement = globalThis.document.createElement("span");
|
||||||
cipherUserLoginElement.classList.add("cipher-user-login");
|
cipherSubtitleElement.classList.add("cipher-subtitle");
|
||||||
cipherUserLoginElement.textContent = cipher.login.username;
|
cipherSubtitleElement.textContent = subTitleText;
|
||||||
cipherUserLoginElement.setAttribute("title", cipher.login.username);
|
cipherSubtitleElement.setAttribute("title", subTitleText);
|
||||||
|
|
||||||
return cipherUserLoginElement;
|
return cipherSubtitleElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -263,7 +263,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cipher-name,
|
.cipher-name,
|
||||||
.cipher-user-login {
|
.cipher-subtitle {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@ -283,7 +283,7 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cipher-user-login {
|
.cipher-subtitle {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
|
|
||||||
@include themify($themes) {
|
@include themify($themes) {
|
||||||
|
@ -19,7 +19,7 @@ export type SubFrameDataFromWindowMessage = SubFrameOffsetData & {
|
|||||||
export type AutofillOverlayContentExtensionMessageHandlers = {
|
export type AutofillOverlayContentExtensionMessageHandlers = {
|
||||||
[key: string]: CallableFunction;
|
[key: string]: CallableFunction;
|
||||||
openAutofillInlineMenu: ({ message }: AutofillExtensionMessageParam) => void;
|
openAutofillInlineMenu: ({ message }: AutofillExtensionMessageParam) => void;
|
||||||
addNewVaultItemFromOverlay: () => void;
|
addNewVaultItemFromOverlay: ({ message }: AutofillExtensionMessageParam) => void;
|
||||||
blurMostRecentlyFocusedField: () => void;
|
blurMostRecentlyFocusedField: () => void;
|
||||||
unsetMostRecentlyFocusedField: () => void;
|
unsetMostRecentlyFocusedField: () => void;
|
||||||
checkIsMostRecentlyFocusedFieldWithinViewport: () => Promise<boolean>;
|
checkIsMostRecentlyFocusedFieldWithinViewport: () => Promise<boolean>;
|
||||||
|
@ -1,6 +1,21 @@
|
|||||||
import AutofillField from "../../models/autofill-field";
|
import AutofillField from "../../models/autofill-field";
|
||||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||||
|
|
||||||
|
export type AutofillKeywordsMap = WeakMap<
|
||||||
|
AutofillField,
|
||||||
|
{
|
||||||
|
keywordsSet: Set<string>;
|
||||||
|
stringValue: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
export interface InlineMenuFieldQualificationService {
|
export interface InlineMenuFieldQualificationService {
|
||||||
isFieldForLoginForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean;
|
isFieldForLoginForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean;
|
||||||
|
isFieldForCreditCardForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean;
|
||||||
|
isFieldForCardholderName(field: AutofillField): boolean;
|
||||||
|
isFieldForCardNumber(field: AutofillField): boolean;
|
||||||
|
isFieldForCardExpirationDate(field: AutofillField): boolean;
|
||||||
|
isFieldForCardExpirationMonth(field: AutofillField): boolean;
|
||||||
|
isFieldForCardExpirationYear(field: AutofillField): boolean;
|
||||||
|
isFieldForCardCvv(field: AutofillField): boolean;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { mock } from "jest-mock-extended";
|
|||||||
|
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { AutofillOverlayVisibility, EVENTS } from "@bitwarden/common/autofill/constants";
|
import { AutofillOverlayVisibility, EVENTS } from "@bitwarden/common/autofill/constants";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
import AutofillInit from "../content/autofill-init";
|
import AutofillInit from "../content/autofill-init";
|
||||||
import {
|
import {
|
||||||
@ -14,7 +15,7 @@ import AutofillForm from "../models/autofill-form";
|
|||||||
import AutofillPageDetails from "../models/autofill-page-details";
|
import AutofillPageDetails from "../models/autofill-page-details";
|
||||||
import { createAutofillFieldMock } from "../spec/autofill-mocks";
|
import { createAutofillFieldMock } from "../spec/autofill-mocks";
|
||||||
import { flushPromises, postWindowMessage, sendMockExtensionMessage } from "../spec/testing-utils";
|
import { flushPromises, postWindowMessage, sendMockExtensionMessage } from "../spec/testing-utils";
|
||||||
import { ElementWithOpId, FormFieldElement } from "../types";
|
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
||||||
|
|
||||||
import { AutoFillConstants } from "./autofill-constants";
|
import { AutoFillConstants } from "./autofill-constants";
|
||||||
import { AutofillOverlayContentService } from "./autofill-overlay-content.service";
|
import { AutofillOverlayContentService } from "./autofill-overlay-content.service";
|
||||||
@ -147,7 +148,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
opid: "password-field",
|
opid: "password-field",
|
||||||
form: "validFormId",
|
form: "validFormId",
|
||||||
elementNumber: 2,
|
elementNumber: 2,
|
||||||
autocompleteType: "current-password",
|
autoCompleteType: "current-password",
|
||||||
type: "password",
|
type: "password",
|
||||||
});
|
});
|
||||||
pageDetailsMock = mock<AutofillPageDetails>({
|
pageDetailsMock = mock<AutofillPageDetails>({
|
||||||
@ -180,7 +181,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores fields that do not appear as a login field", async () => {
|
it("ignores fields that do not appear as a login or card field", async () => {
|
||||||
autofillFieldData.htmlName = "another-type-of-field";
|
autofillFieldData.htmlName = "another-type-of-field";
|
||||||
autofillFieldData.htmlID = "another-type-of-field";
|
autofillFieldData.htmlID = "another-type-of-field";
|
||||||
autofillFieldData.placeholder = "another-type-of-field";
|
autofillFieldData.placeholder = "another-type-of-field";
|
||||||
@ -196,7 +197,10 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("skips setup on fields that have been previously set up", async () => {
|
it("skips setup on fields that have been previously set up", async () => {
|
||||||
autofillOverlayContentService["formFieldElements"].add(autofillFieldElement);
|
autofillOverlayContentService["formFieldElements"].set(
|
||||||
|
autofillFieldElement,
|
||||||
|
autofillFieldData,
|
||||||
|
);
|
||||||
|
|
||||||
await autofillOverlayContentService.setupInlineMenu(
|
await autofillOverlayContentService.setupInlineMenu(
|
||||||
autofillFieldElement,
|
autofillFieldElement,
|
||||||
@ -414,6 +418,17 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
expect(autofillOverlayContentService["storeModifiedFormElement"]).not.toHaveBeenCalled();
|
expect(autofillOverlayContentService["storeModifiedFormElement"]).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("skips storing the element if it is not present in the set of qualified autofill fields", () => {
|
||||||
|
const randomElement = document.createElement(
|
||||||
|
"input",
|
||||||
|
) as ElementWithOpId<FillableFormFieldElement>;
|
||||||
|
jest.spyOn(autofillOverlayContentService as any, "storeUserFilledLoginField");
|
||||||
|
|
||||||
|
autofillOverlayContentService["storeModifiedFormElement"](randomElement);
|
||||||
|
|
||||||
|
expect(autofillOverlayContentService["storeUserFilledLoginField"]).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("sets the field as the most recently focused form field element", async () => {
|
it("sets the field as the most recently focused form field element", async () => {
|
||||||
autofillOverlayContentService["mostRecentlyFocusedField"] =
|
autofillOverlayContentService["mostRecentlyFocusedField"] =
|
||||||
mock<ElementWithOpId<FormFieldElement>>();
|
mock<ElementWithOpId<FormFieldElement>>();
|
||||||
@ -551,6 +566,155 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
|
|
||||||
expect(autofillOverlayContentService["openInlineMenu"]).toHaveBeenCalled();
|
expect(autofillOverlayContentService["openInlineMenu"]).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("input changes on a field filled by a card cipher", () => {
|
||||||
|
let inputFieldElement: ElementWithOpId<FillableFormFieldElement>;
|
||||||
|
let inputFieldData: AutofillField;
|
||||||
|
let selectFieldElement: ElementWithOpId<FillableFormFieldElement>;
|
||||||
|
let selectFieldData: AutofillField;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
inputFieldElement = document.createElement(
|
||||||
|
"input",
|
||||||
|
) as ElementWithOpId<FillableFormFieldElement>;
|
||||||
|
inputFieldData = createAutofillFieldMock({
|
||||||
|
opid: "input-field",
|
||||||
|
form: "validFormId",
|
||||||
|
elementNumber: 3,
|
||||||
|
autoCompleteType: "cc-number",
|
||||||
|
type: "text",
|
||||||
|
filledByCipherType: CipherType.Card,
|
||||||
|
viewable: true,
|
||||||
|
});
|
||||||
|
selectFieldElement = document.createElement(
|
||||||
|
"select",
|
||||||
|
) as ElementWithOpId<FillableFormFieldElement>;
|
||||||
|
selectFieldData = createAutofillFieldMock({
|
||||||
|
opid: "select-field",
|
||||||
|
form: "validFormId",
|
||||||
|
elementNumber: 4,
|
||||||
|
autoCompleteType: "cc-type",
|
||||||
|
type: "select",
|
||||||
|
filledByCipherType: CipherType.Card,
|
||||||
|
viewable: true,
|
||||||
|
});
|
||||||
|
pageDetailsMock.fields = [inputFieldData, selectFieldData];
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only stores the element if the form field is a select element", async () => {
|
||||||
|
jest.spyOn(autofillOverlayContentService as any, "storeModifiedFormElement");
|
||||||
|
jest.spyOn(autofillOverlayContentService as any, "hideInlineMenuListOnFilledField");
|
||||||
|
|
||||||
|
await autofillOverlayContentService.setupInlineMenu(
|
||||||
|
selectFieldElement,
|
||||||
|
selectFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
selectFieldElement.dispatchEvent(new Event("input"));
|
||||||
|
|
||||||
|
expect(autofillOverlayContentService["storeModifiedFormElement"]).toHaveBeenCalledWith(
|
||||||
|
selectFieldElement,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
autofillOverlayContentService["hideInlineMenuListOnFilledField"],
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores cardholder name fields", async () => {
|
||||||
|
inputFieldData.autoCompleteType = "cc-name";
|
||||||
|
|
||||||
|
await autofillOverlayContentService.setupInlineMenu(
|
||||||
|
inputFieldElement,
|
||||||
|
inputFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
inputFieldElement.dispatchEvent(new Event("input"));
|
||||||
|
|
||||||
|
expect(autofillOverlayContentService["userFilledFields"].cardholderName).toEqual(
|
||||||
|
inputFieldElement,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores card number fields", async () => {
|
||||||
|
await autofillOverlayContentService.setupInlineMenu(
|
||||||
|
inputFieldElement,
|
||||||
|
inputFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
inputFieldElement.dispatchEvent(new Event("input"));
|
||||||
|
|
||||||
|
expect(autofillOverlayContentService["userFilledFields"].cardNumber).toEqual(
|
||||||
|
inputFieldElement,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores card expiration month fields", async () => {
|
||||||
|
inputFieldData.autoCompleteType = "cc-exp-month";
|
||||||
|
|
||||||
|
await autofillOverlayContentService.setupInlineMenu(
|
||||||
|
inputFieldElement,
|
||||||
|
inputFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
inputFieldElement.dispatchEvent(new Event("input"));
|
||||||
|
|
||||||
|
expect(autofillOverlayContentService["userFilledFields"].cardExpirationMonth).toEqual(
|
||||||
|
inputFieldElement,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores card expiration year fields", async () => {
|
||||||
|
inputFieldData.autoCompleteType = "cc-exp-year";
|
||||||
|
|
||||||
|
await autofillOverlayContentService.setupInlineMenu(
|
||||||
|
inputFieldElement,
|
||||||
|
inputFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
inputFieldElement.dispatchEvent(new Event("input"));
|
||||||
|
|
||||||
|
expect(autofillOverlayContentService["userFilledFields"].cardExpirationYear).toEqual(
|
||||||
|
inputFieldElement,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores card expiration date fields", async () => {
|
||||||
|
inputFieldData.autoCompleteType = "cc-exp";
|
||||||
|
|
||||||
|
await autofillOverlayContentService.setupInlineMenu(
|
||||||
|
inputFieldElement,
|
||||||
|
inputFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
inputFieldElement.dispatchEvent(new Event("input"));
|
||||||
|
|
||||||
|
expect(autofillOverlayContentService["userFilledFields"].cardExpirationDate).toEqual(
|
||||||
|
inputFieldElement,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores card cvv fields", async () => {
|
||||||
|
inputFieldData.autoCompleteType = "cc-csc";
|
||||||
|
|
||||||
|
await autofillOverlayContentService.setupInlineMenu(
|
||||||
|
inputFieldElement,
|
||||||
|
inputFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
inputFieldElement.dispatchEvent(new Event("input"));
|
||||||
|
|
||||||
|
expect(autofillOverlayContentService["userFilledFields"].cardCvv).toEqual(
|
||||||
|
inputFieldElement,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("form field click event listener", () => {
|
describe("form field click event listener", () => {
|
||||||
@ -627,6 +791,24 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
expect(updateMostRecentlyFocusedFieldSpy).not.toHaveBeenCalled();
|
expect(updateMostRecentlyFocusedFieldSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("closes the inline menu if the focused element is a select element", async () => {
|
||||||
|
const selectFieldElement = document.createElement(
|
||||||
|
"select",
|
||||||
|
) as ElementWithOpId<HTMLSelectElement>;
|
||||||
|
autofillFieldData.type = "select";
|
||||||
|
autofillFieldData.autoCompleteType = "cc-type";
|
||||||
|
await autofillOverlayContentService.setupInlineMenu(
|
||||||
|
selectFieldElement,
|
||||||
|
autofillFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
selectFieldElement.dispatchEvent(new Event("focus"));
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu");
|
||||||
|
});
|
||||||
|
|
||||||
it("updates the most recently focused field", async () => {
|
it("updates the most recently focused field", async () => {
|
||||||
await autofillOverlayContentService.setupInlineMenu(
|
await autofillOverlayContentService.setupInlineMenu(
|
||||||
autofillFieldElement,
|
autofillFieldElement,
|
||||||
@ -801,6 +983,85 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
expect(autofillFieldElement.removeEventListener).toHaveBeenCalled();
|
expect(autofillFieldElement.removeEventListener).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("setting up the form field listeners on card fields", () => {
|
||||||
|
const inputCardFieldData = createAutofillFieldMock({
|
||||||
|
opid: "card-field",
|
||||||
|
form: "validFormId",
|
||||||
|
elementNumber: 3,
|
||||||
|
autoCompleteType: "cc-number",
|
||||||
|
type: "text",
|
||||||
|
});
|
||||||
|
const selectCardFieldData = createAutofillFieldMock({
|
||||||
|
opid: "card-field",
|
||||||
|
form: "validFormId",
|
||||||
|
elementNumber: 3,
|
||||||
|
autoCompleteType: "cc-type",
|
||||||
|
type: "select-one",
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pageDetailsMock.fields = [inputCardFieldData, selectCardFieldData];
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets up the input card field listeners", async () => {
|
||||||
|
await autofillOverlayContentService.setupInlineMenu(
|
||||||
|
autofillFieldElement,
|
||||||
|
inputCardFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(autofillFieldElement.addEventListener).toHaveBeenCalledWith(
|
||||||
|
EVENTS.BLUR,
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
expect(autofillFieldElement.addEventListener).toHaveBeenCalledWith(
|
||||||
|
EVENTS.KEYUP,
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
expect(autofillFieldElement.addEventListener).toHaveBeenCalledWith(
|
||||||
|
EVENTS.INPUT,
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
expect(autofillFieldElement.addEventListener).toHaveBeenCalledWith(
|
||||||
|
EVENTS.CLICK,
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
expect(autofillFieldElement.addEventListener).toHaveBeenCalledWith(
|
||||||
|
EVENTS.FOCUS,
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
expect(autofillFieldElement.removeEventListener).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets up the input and focus listeners on a select card field", async () => {
|
||||||
|
const selectCardFieldElement = document.createElement(
|
||||||
|
"select",
|
||||||
|
) as ElementWithOpId<HTMLSelectElement>;
|
||||||
|
selectCardFieldElement.opid = "op-2";
|
||||||
|
jest.spyOn(selectCardFieldElement, "addEventListener");
|
||||||
|
|
||||||
|
await autofillOverlayContentService.setupInlineMenu(
|
||||||
|
selectCardFieldElement,
|
||||||
|
selectCardFieldData,
|
||||||
|
pageDetailsMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(selectCardFieldElement.addEventListener).toHaveBeenCalledWith(
|
||||||
|
EVENTS.FOCUS,
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
expect(selectCardFieldElement.addEventListener).toHaveBeenCalledWith(
|
||||||
|
EVENTS.INPUT,
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
expect(selectCardFieldElement.addEventListener).not.toHaveBeenCalledWith(
|
||||||
|
EVENTS.KEYUP,
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips triggering the form field focused handler if the document is not focused", async () => {
|
it("skips triggering the form field focused handler if the document is not focused", async () => {
|
||||||
@ -1065,10 +1326,14 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
|
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
|
||||||
.mockResolvedValue(true);
|
.mockResolvedValue(true);
|
||||||
|
|
||||||
sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" });
|
sendMockExtensionMessage({
|
||||||
|
command: "addNewVaultItemFromOverlay",
|
||||||
|
addNewCipherType: CipherType.Login,
|
||||||
|
});
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayAddNewVaultItem", {
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayAddNewVaultItem", {
|
||||||
|
addNewCipherType: CipherType.Login,
|
||||||
login: {
|
login: {
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
@ -1101,10 +1366,14 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
password: passwordField,
|
password: passwordField,
|
||||||
};
|
};
|
||||||
|
|
||||||
sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" });
|
sendMockExtensionMessage({
|
||||||
|
command: "addNewVaultItemFromOverlay",
|
||||||
|
addNewCipherType: CipherType.Login,
|
||||||
|
});
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayAddNewVaultItem", {
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayAddNewVaultItem", {
|
||||||
|
addNewCipherType: CipherType.Login,
|
||||||
login: {
|
login: {
|
||||||
username: "test-username",
|
username: "test-username",
|
||||||
password: "test-password",
|
password: "test-password",
|
||||||
@ -1113,6 +1382,30 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sends a message that facilitates adding a card cipher vault item", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
|
||||||
|
.mockResolvedValue(true);
|
||||||
|
|
||||||
|
sendMockExtensionMessage({
|
||||||
|
command: "addNewVaultItemFromOverlay",
|
||||||
|
addNewCipherType: CipherType.Card,
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayAddNewVaultItem", {
|
||||||
|
addNewCipherType: CipherType.Card,
|
||||||
|
card: {
|
||||||
|
cardholderName: "",
|
||||||
|
cvv: "",
|
||||||
|
expirationDate: "",
|
||||||
|
expirationMonth: "",
|
||||||
|
expirationYear: "",
|
||||||
|
number: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("unsetMostRecentlyFocusedField message handler", () => {
|
describe("unsetMostRecentlyFocusedField message handler", () => {
|
||||||
@ -1138,6 +1431,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
autofillOverlayContentService["focusedFieldData"] = {
|
autofillOverlayContentService["focusedFieldData"] = {
|
||||||
focusedFieldStyles: { paddingRight: "10", paddingLeft: "10" },
|
focusedFieldStyles: { paddingRight: "10", paddingLeft: "10" },
|
||||||
focusedFieldRects: { width: 10, height: 10, top: 10, left: 10 },
|
focusedFieldRects: { width: 10, height: 10, top: 10, left: 10 },
|
||||||
|
filledByCipherType: CipherType.Login,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1587,7 +1881,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("skips setup when no form fields exist on the current frame", async () => {
|
it("skips setup when no form fields exist on the current frame", async () => {
|
||||||
autofillOverlayContentService["formFieldElements"] = new Set();
|
autofillOverlayContentService["formFieldElements"] = new Map();
|
||||||
|
|
||||||
sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" });
|
sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" });
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
@ -1604,7 +1898,10 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sets up the sub frame rebuild listeners when the sub frame contains fields", async () => {
|
it("sets up the sub frame rebuild listeners when the sub frame contains fields", async () => {
|
||||||
autofillOverlayContentService["formFieldElements"].add(autofillFieldElement);
|
autofillOverlayContentService["formFieldElements"].set(
|
||||||
|
autofillFieldElement,
|
||||||
|
createAutofillFieldMock(),
|
||||||
|
);
|
||||||
|
|
||||||
sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" });
|
sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" });
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
@ -1621,7 +1918,10 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
|
|
||||||
describe("triggering the sub frame listener", () => {
|
describe("triggering the sub frame listener", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
autofillOverlayContentService["formFieldElements"].add(autofillFieldElement);
|
autofillOverlayContentService["formFieldElements"].set(
|
||||||
|
autofillFieldElement,
|
||||||
|
createAutofillFieldMock(),
|
||||||
|
);
|
||||||
await sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" });
|
await sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" });
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1672,7 +1972,7 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
opid: "password-field",
|
opid: "password-field",
|
||||||
form: "validFormId",
|
form: "validFormId",
|
||||||
elementNumber: 2,
|
elementNumber: 2,
|
||||||
autocompleteType: "current-password",
|
autoCompleteType: "current-password",
|
||||||
type: "password",
|
type: "password",
|
||||||
});
|
});
|
||||||
pageDetailsMock = mock<AutofillPageDetails>({
|
pageDetailsMock = mock<AutofillPageDetails>({
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
AutofillOverlayVisibility,
|
AutofillOverlayVisibility,
|
||||||
AUTOFILL_OVERLAY_HANDLE_REPOSITION,
|
AUTOFILL_OVERLAY_HANDLE_REPOSITION,
|
||||||
} from "@bitwarden/common/autofill/constants";
|
} from "@bitwarden/common/autofill/constants";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FocusedFieldData,
|
FocusedFieldData,
|
||||||
@ -24,6 +25,7 @@ import AutofillPageDetails from "../models/autofill-page-details";
|
|||||||
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
||||||
import {
|
import {
|
||||||
elementIsFillableFormField,
|
elementIsFillableFormField,
|
||||||
|
elementIsSelectElement,
|
||||||
getAttributeBoolean,
|
getAttributeBoolean,
|
||||||
sendExtensionMessage,
|
sendExtensionMessage,
|
||||||
throttle,
|
throttle,
|
||||||
@ -43,7 +45,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
inlineMenuVisibility: number;
|
inlineMenuVisibility: number;
|
||||||
private readonly findTabs = tabbable;
|
private readonly findTabs = tabbable;
|
||||||
private readonly sendExtensionMessage = sendExtensionMessage;
|
private readonly sendExtensionMessage = sendExtensionMessage;
|
||||||
private formFieldElements: Set<ElementWithOpId<FormFieldElement>> = new Set([]);
|
private formFieldElements: Map<ElementWithOpId<FormFieldElement>, AutofillField> = new Map();
|
||||||
private hiddenFormFieldElements: WeakMap<ElementWithOpId<FormFieldElement>, AutofillField> =
|
private hiddenFormFieldElements: WeakMap<ElementWithOpId<FormFieldElement>, AutofillField> =
|
||||||
new WeakMap();
|
new WeakMap();
|
||||||
private ignoredFieldTypes: Set<string> = new Set(AutoFillConstants.ExcludedInlineMenuTypes);
|
private ignoredFieldTypes: Set<string> = new Set(AutoFillConstants.ExcludedInlineMenuTypes);
|
||||||
@ -57,7 +59,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
private eventHandlersMemo: { [key: string]: EventListener } = {};
|
private eventHandlersMemo: { [key: string]: EventListener } = {};
|
||||||
private readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = {
|
private readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = {
|
||||||
openAutofillInlineMenu: ({ message }) => this.openInlineMenu(message),
|
openAutofillInlineMenu: ({ message }) => this.openInlineMenu(message),
|
||||||
addNewVaultItemFromOverlay: () => this.addNewVaultItem(),
|
addNewVaultItemFromOverlay: ({ message }) => this.addNewVaultItem(message),
|
||||||
blurMostRecentlyFocusedField: () => this.blurMostRecentlyFocusedField(),
|
blurMostRecentlyFocusedField: () => this.blurMostRecentlyFocusedField(),
|
||||||
unsetMostRecentlyFocusedField: () => this.unsetMostRecentlyFocusedField(),
|
unsetMostRecentlyFocusedField: () => this.unsetMostRecentlyFocusedField(),
|
||||||
checkIsMostRecentlyFocusedFieldWithinViewport: () =>
|
checkIsMostRecentlyFocusedFieldWithinViewport: () =>
|
||||||
@ -122,7 +124,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.setupInlineMenuOnQualifiedField(formFieldElement);
|
await this.setupInlineMenuOnQualifiedField(formFieldElement, autofillFieldData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -194,11 +196,14 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
* Formats any found user filled fields for a login cipher and sends a message
|
* Formats any found user filled fields for a login cipher and sends a message
|
||||||
* to the background script to add a new cipher.
|
* to the background script to add a new cipher.
|
||||||
*/
|
*/
|
||||||
async addNewVaultItem() {
|
async addNewVaultItem({ addNewCipherType }: AutofillExtensionMessage) {
|
||||||
if (!(await this.isInlineMenuListVisible())) {
|
if (!(await this.isInlineMenuListVisible())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const command = "autofillOverlayAddNewVaultItem";
|
||||||
|
|
||||||
|
if (addNewCipherType === CipherType.Login) {
|
||||||
const login = {
|
const login = {
|
||||||
username: this.userFilledFields["username"]?.value || "",
|
username: this.userFilledFields["username"]?.value || "",
|
||||||
password: this.userFilledFields["password"]?.value || "",
|
password: this.userFilledFields["password"]?.value || "",
|
||||||
@ -206,7 +211,23 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
hostname: globalThis.document.location.hostname,
|
hostname: globalThis.document.location.hostname,
|
||||||
};
|
};
|
||||||
|
|
||||||
void this.sendExtensionMessage("autofillOverlayAddNewVaultItem", { login });
|
void this.sendExtensionMessage(command, { addNewCipherType, login });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addNewCipherType === CipherType.Card) {
|
||||||
|
const card = {
|
||||||
|
cardholderName: this.userFilledFields["cardholderName"]?.value || "",
|
||||||
|
number: this.userFilledFields["cardNumber"]?.value || "",
|
||||||
|
expirationMonth: this.userFilledFields["cardExpirationMonth"]?.value || "",
|
||||||
|
expirationYear: this.userFilledFields["cardExpirationYear"]?.value || "",
|
||||||
|
expirationDate: this.userFilledFields["cardExpirationDate"]?.value || "",
|
||||||
|
cvv: this.userFilledFields["cardCvv"]?.value || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
void this.sendExtensionMessage(command, { addNewCipherType, card });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -253,20 +274,25 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
private setupFormFieldElementEventListeners(formFieldElement: ElementWithOpId<FormFieldElement>) {
|
private setupFormFieldElementEventListeners(formFieldElement: ElementWithOpId<FormFieldElement>) {
|
||||||
this.removeCachedFormFieldEventListeners(formFieldElement);
|
this.removeCachedFormFieldEventListeners(formFieldElement);
|
||||||
|
|
||||||
formFieldElement.addEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent);
|
|
||||||
formFieldElement.addEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent);
|
|
||||||
formFieldElement.addEventListener(
|
formFieldElement.addEventListener(
|
||||||
EVENTS.INPUT,
|
EVENTS.INPUT,
|
||||||
this.handleFormFieldInputEvent(formFieldElement),
|
this.handleFormFieldInputEvent(formFieldElement),
|
||||||
);
|
);
|
||||||
formFieldElement.addEventListener(
|
|
||||||
EVENTS.CLICK,
|
|
||||||
this.handleFormFieldClickEvent(formFieldElement),
|
|
||||||
);
|
|
||||||
formFieldElement.addEventListener(
|
formFieldElement.addEventListener(
|
||||||
EVENTS.FOCUS,
|
EVENTS.FOCUS,
|
||||||
this.handleFormFieldFocusEvent(formFieldElement),
|
this.handleFormFieldFocusEvent(formFieldElement),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (elementIsSelectElement(formFieldElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formFieldElement.addEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent);
|
||||||
|
formFieldElement.addEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent);
|
||||||
|
formFieldElement.addEventListener(
|
||||||
|
EVENTS.CLICK,
|
||||||
|
this.handleFormFieldClickEvent(formFieldElement),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -400,6 +426,9 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.storeModifiedFormElement(formFieldElement);
|
this.storeModifiedFormElement(formFieldElement);
|
||||||
|
if (elementIsSelectElement(formFieldElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (await this.hideInlineMenuListOnFilledField(formFieldElement)) {
|
if (await this.hideInlineMenuListOnFilledField(formFieldElement)) {
|
||||||
void this.sendExtensionMessage("closeAutofillInlineMenu", {
|
void this.sendExtensionMessage("closeAutofillInlineMenu", {
|
||||||
@ -425,6 +454,26 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
void this.updateMostRecentlyFocusedField(formFieldElement);
|
void this.updateMostRecentlyFocusedField(formFieldElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const autofillFieldData = this.formFieldElements.get(formFieldElement);
|
||||||
|
if (!autofillFieldData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autofillFieldData.filledByCipherType === CipherType.Login) {
|
||||||
|
this.storeUserFilledLoginField(formFieldElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autofillFieldData.filledByCipherType === CipherType.Card) {
|
||||||
|
this.storeUserFilledCardField(formFieldElement, autofillFieldData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles storing the user field login field to be used when adding a new vault item.
|
||||||
|
*
|
||||||
|
* @param formFieldElement - The form field element that triggered the input event.
|
||||||
|
*/
|
||||||
|
private storeUserFilledLoginField(formFieldElement: ElementWithOpId<FillableFormFieldElement>) {
|
||||||
if (formFieldElement.type === "password") {
|
if (formFieldElement.type === "password") {
|
||||||
this.userFilledFields.password = formFieldElement;
|
this.userFilledFields.password = formFieldElement;
|
||||||
return;
|
return;
|
||||||
@ -433,6 +482,46 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
this.userFilledFields.username = formFieldElement;
|
this.userFilledFields.username = formFieldElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles storing the user field card field to be used when adding a new vault item.
|
||||||
|
*
|
||||||
|
* @param formFieldElement - The form field element that triggered the input event.
|
||||||
|
* @param autofillFieldData - Autofill field data captured from the form field element.
|
||||||
|
*/
|
||||||
|
private storeUserFilledCardField(
|
||||||
|
formFieldElement: ElementWithOpId<FillableFormFieldElement>,
|
||||||
|
autofillFieldData: AutofillField,
|
||||||
|
) {
|
||||||
|
if (this.inlineMenuFieldQualificationService.isFieldForCardholderName(autofillFieldData)) {
|
||||||
|
this.userFilledFields.cardholderName = formFieldElement;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inlineMenuFieldQualificationService.isFieldForCardNumber(autofillFieldData)) {
|
||||||
|
this.userFilledFields.cardNumber = formFieldElement;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inlineMenuFieldQualificationService.isFieldForCardExpirationMonth(autofillFieldData)) {
|
||||||
|
this.userFilledFields.cardExpirationMonth = formFieldElement;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inlineMenuFieldQualificationService.isFieldForCardExpirationYear(autofillFieldData)) {
|
||||||
|
this.userFilledFields.cardExpirationYear = formFieldElement;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inlineMenuFieldQualificationService.isFieldForCardExpirationDate(autofillFieldData)) {
|
||||||
|
this.userFilledFields.cardExpirationDate = formFieldElement;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inlineMenuFieldQualificationService.isFieldForCardCvv(autofillFieldData)) {
|
||||||
|
this.userFilledFields.cardCvv = formFieldElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up and memoizes the form field click event handler.
|
* Sets up and memoizes the form field click event handler.
|
||||||
*
|
*
|
||||||
@ -483,16 +572,23 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (elementIsSelectElement(formFieldElement)) {
|
||||||
|
await this.sendExtensionMessage("closeAutofillInlineMenu");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.sendExtensionMessage("updateIsFieldCurrentlyFocused", {
|
await this.sendExtensionMessage("updateIsFieldCurrentlyFocused", {
|
||||||
isFieldCurrentlyFocused: true,
|
isFieldCurrentlyFocused: true,
|
||||||
});
|
});
|
||||||
const initiallyFocusedField = this.mostRecentlyFocusedField;
|
const initiallyFocusedField = this.mostRecentlyFocusedField;
|
||||||
await this.updateMostRecentlyFocusedField(formFieldElement);
|
await this.updateMostRecentlyFocusedField(formFieldElement);
|
||||||
|
|
||||||
|
const hideInlineMenuListOnFilledField = await this.hideInlineMenuListOnFilledField(
|
||||||
|
formFieldElement as FillableFormFieldElement,
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick ||
|
this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick ||
|
||||||
(initiallyFocusedField !== this.mostRecentlyFocusedField &&
|
(initiallyFocusedField !== this.mostRecentlyFocusedField && hideInlineMenuListOnFilledField)
|
||||||
(await this.hideInlineMenuListOnFilledField(formFieldElement as FillableFormFieldElement)))
|
|
||||||
) {
|
) {
|
||||||
await this.sendExtensionMessage("closeAutofillInlineMenu", {
|
await this.sendExtensionMessage("closeAutofillInlineMenu", {
|
||||||
overlayElement: AutofillOverlayElement.List,
|
overlayElement: AutofillOverlayElement.List,
|
||||||
@ -500,7 +596,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.hideInlineMenuListOnFilledField(formFieldElement as FillableFormFieldElement)) {
|
if (hideInlineMenuListOnFilledField) {
|
||||||
this.updateInlineMenuButtonPosition();
|
this.updateInlineMenuButtonPosition();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -568,9 +664,11 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
const { paddingRight, paddingLeft } = globalThis.getComputedStyle(formFieldElement);
|
const { paddingRight, paddingLeft } = globalThis.getComputedStyle(formFieldElement);
|
||||||
const { width, height, top, left } =
|
const { width, height, top, left } =
|
||||||
await this.getMostRecentlyFocusedFieldRects(formFieldElement);
|
await this.getMostRecentlyFocusedFieldRects(formFieldElement);
|
||||||
|
const autofillFieldData = this.formFieldElements.get(formFieldElement);
|
||||||
this.focusedFieldData = {
|
this.focusedFieldData = {
|
||||||
focusedFieldStyles: { paddingRight, paddingLeft },
|
focusedFieldStyles: { paddingRight, paddingLeft },
|
||||||
focusedFieldRects: { width, height, top, left },
|
focusedFieldRects: { width, height, top, left },
|
||||||
|
filledByCipherType: autofillFieldData?.filledByCipherType,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.sendExtensionMessage("updateFocusedFieldData", {
|
await this.sendExtensionMessage("updateFocusedFieldData", {
|
||||||
@ -648,10 +746,24 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !this.inlineMenuFieldQualificationService.isFieldForLoginForm(
|
if (
|
||||||
|
this.inlineMenuFieldQualificationService.isFieldForLoginForm(autofillFieldData, pageDetails)
|
||||||
|
) {
|
||||||
|
autofillFieldData.filledByCipherType = CipherType.Login;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.inlineMenuFieldQualificationService.isFieldForCreditCardForm(
|
||||||
autofillFieldData,
|
autofillFieldData,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
);
|
)
|
||||||
|
) {
|
||||||
|
autofillFieldData.filledByCipherType = CipherType.Card;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -714,7 +826,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
autofillFieldData.readonly = getAttributeBoolean(formFieldElement, "disabled");
|
autofillFieldData.readonly = getAttributeBoolean(formFieldElement, "disabled");
|
||||||
autofillFieldData.disabled = getAttributeBoolean(formFieldElement, "disabled");
|
autofillFieldData.disabled = getAttributeBoolean(formFieldElement, "disabled");
|
||||||
autofillFieldData.viewable = true;
|
autofillFieldData.viewable = true;
|
||||||
void this.setupInlineMenuOnQualifiedField(formFieldElement);
|
void this.setupInlineMenuOnQualifiedField(formFieldElement, autofillFieldData);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.removeHiddenFieldFallbackListener(formFieldElement);
|
this.removeHiddenFieldFallbackListener(formFieldElement);
|
||||||
@ -724,11 +836,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
* Sets up the inline menu on a qualified form field element.
|
* Sets up the inline menu on a qualified form field element.
|
||||||
*
|
*
|
||||||
* @param formFieldElement - The form field element to set up the inline menu on.
|
* @param formFieldElement - The form field element to set up the inline menu on.
|
||||||
|
* @param autofillFieldData - Autofill field data captured from the form field element.
|
||||||
*/
|
*/
|
||||||
private async setupInlineMenuOnQualifiedField(
|
private async setupInlineMenuOnQualifiedField(
|
||||||
formFieldElement: ElementWithOpId<FormFieldElement>,
|
formFieldElement: ElementWithOpId<FormFieldElement>,
|
||||||
|
autofillFieldData: AutofillField,
|
||||||
) {
|
) {
|
||||||
this.formFieldElements.add(formFieldElement);
|
this.formFieldElements.set(formFieldElement, autofillFieldData);
|
||||||
|
|
||||||
if (!this.mostRecentlyFocusedField) {
|
if (!this.mostRecentlyFocusedField) {
|
||||||
await this.updateMostRecentlyFocusedField(formFieldElement);
|
await this.updateMostRecentlyFocusedField(formFieldElement);
|
||||||
@ -1198,7 +1312,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
destroy() {
|
destroy() {
|
||||||
this.clearFocusInlineMenuListTimeout();
|
this.clearFocusInlineMenuListTimeout();
|
||||||
this.clearCloseInlineMenuOnRedirectTimeout();
|
this.clearCloseInlineMenuOnRedirectTimeout();
|
||||||
this.formFieldElements.forEach((formFieldElement) => {
|
this.formFieldElements.forEach((_autofillField, formFieldElement) => {
|
||||||
this.removeCachedFormFieldEventListeners(formFieldElement);
|
this.removeCachedFormFieldEventListeners(formFieldElement);
|
||||||
formFieldElement.removeEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent);
|
formFieldElement.removeEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent);
|
||||||
formFieldElement.removeEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent);
|
formFieldElement.removeEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent);
|
||||||
|
@ -713,4 +713,155 @@ describe("InlineMenuFieldQualificationService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("isFieldForCreditCardForm", () => {
|
||||||
|
describe("an invalid credit card field", () => {
|
||||||
|
it("has reference to a `new field` keyword", () => {
|
||||||
|
const field = mock<AutofillField>({
|
||||||
|
placeholder: "new credit card",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("does not have a parent form", () => {
|
||||||
|
it("has no credit card number fields in the page details", () => {
|
||||||
|
const field = mock<AutofillField>({
|
||||||
|
placeholder: "name",
|
||||||
|
});
|
||||||
|
const secondField = mock<AutofillField>({
|
||||||
|
placeholder: "card cvv",
|
||||||
|
autoCompleteType: "cc-csc",
|
||||||
|
});
|
||||||
|
pageDetails.forms = {};
|
||||||
|
pageDetails.fields = [field, secondField];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has no credit card cvv fields in the page details", () => {
|
||||||
|
const field = mock<AutofillField>({
|
||||||
|
placeholder: "name",
|
||||||
|
});
|
||||||
|
const secondField = mock<AutofillField>({
|
||||||
|
placeholder: "card number",
|
||||||
|
autoCompleteType: "cc-number",
|
||||||
|
});
|
||||||
|
pageDetails.forms = {};
|
||||||
|
pageDetails.fields = [field, secondField];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("has a parent form", () => {
|
||||||
|
let form: MockProxy<AutofillForm>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
form = mock<AutofillForm>({ opid: "validFormId" });
|
||||||
|
pageDetails.forms = {
|
||||||
|
validFormId: form,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not have a credit card number field within the same form", () => {
|
||||||
|
const field = mock<AutofillField>({
|
||||||
|
placeholder: "name",
|
||||||
|
form: "validFormId",
|
||||||
|
});
|
||||||
|
const cardCvvField = mock<AutofillField>({
|
||||||
|
placeholder: "card cvv",
|
||||||
|
autoCompleteType: "cc-csc",
|
||||||
|
form: "validFormId",
|
||||||
|
});
|
||||||
|
pageDetails.fields = [field, cardCvvField];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not contain a cvv field within the same form", () => {
|
||||||
|
const field = mock<AutofillField>({
|
||||||
|
placeholder: "name",
|
||||||
|
form: "validFormId",
|
||||||
|
});
|
||||||
|
const cardNumberField = mock<AutofillField>({
|
||||||
|
placeholder: "card number",
|
||||||
|
autoCompleteType: "cc-number",
|
||||||
|
form: "validFormId",
|
||||||
|
});
|
||||||
|
|
||||||
|
pageDetails.fields = [field, cardNumberField];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("a valid credit card field", () => {
|
||||||
|
describe("does not have a parent form", () => {
|
||||||
|
it("is structured on a page with a single credit card number field and a single cvv field", () => {
|
||||||
|
const field = mock<AutofillField>({
|
||||||
|
placeholder: "name",
|
||||||
|
});
|
||||||
|
const cardNumberField = mock<AutofillField>({
|
||||||
|
placeholder: "card number",
|
||||||
|
autoCompleteType: "cc-number",
|
||||||
|
});
|
||||||
|
const cardCvvField = mock<AutofillField>({
|
||||||
|
placeholder: "card cvv",
|
||||||
|
autoCompleteType: "cc-csc",
|
||||||
|
});
|
||||||
|
pageDetails.forms = {};
|
||||||
|
pageDetails.fields = [field, cardNumberField, cardCvvField];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("has a parent form", () => {
|
||||||
|
let form: MockProxy<AutofillForm>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
form = mock<AutofillForm>({ opid: "validFormId" });
|
||||||
|
pageDetails.forms = {
|
||||||
|
validFormId: form,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has a credit card number field and cvv field structured within the same form", () => {
|
||||||
|
const field = mock<AutofillField>({
|
||||||
|
placeholder: "name",
|
||||||
|
form: "validFormId",
|
||||||
|
});
|
||||||
|
const cardNumberField = mock<AutofillField>({
|
||||||
|
placeholder: "card number",
|
||||||
|
autoCompleteType: "cc-number",
|
||||||
|
form: "validFormId",
|
||||||
|
});
|
||||||
|
const cardCvvField = mock<AutofillField>({
|
||||||
|
placeholder: "card cvv",
|
||||||
|
autoCompleteType: "cc-csc",
|
||||||
|
form: "validFormId",
|
||||||
|
});
|
||||||
|
pageDetails.fields = [field, cardNumberField, cardCvvField];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,8 +2,11 @@ import AutofillField from "../models/autofill-field";
|
|||||||
import AutofillPageDetails from "../models/autofill-page-details";
|
import AutofillPageDetails from "../models/autofill-page-details";
|
||||||
import { sendExtensionMessage } from "../utils";
|
import { sendExtensionMessage } from "../utils";
|
||||||
|
|
||||||
import { InlineMenuFieldQualificationService as InlineMenuFieldQualificationServiceInterface } from "./abstractions/inline-menu-field-qualifications.service";
|
import {
|
||||||
import { AutoFillConstants } from "./autofill-constants";
|
AutofillKeywordsMap,
|
||||||
|
InlineMenuFieldQualificationService as InlineMenuFieldQualificationServiceInterface,
|
||||||
|
} from "./abstractions/inline-menu-field-qualifications.service";
|
||||||
|
import { AutoFillConstants, CreditCardAutoFillConstants } from "./autofill-constants";
|
||||||
|
|
||||||
export class InlineMenuFieldQualificationService
|
export class InlineMenuFieldQualificationService
|
||||||
implements InlineMenuFieldQualificationServiceInterface
|
implements InlineMenuFieldQualificationServiceInterface
|
||||||
@ -14,9 +17,13 @@ export class InlineMenuFieldQualificationService
|
|||||||
private usernameAutocompleteValues = new Set(["username", "email"]);
|
private usernameAutocompleteValues = new Set(["username", "email"]);
|
||||||
private fieldIgnoreListString = AutoFillConstants.FieldIgnoreList.join(",");
|
private fieldIgnoreListString = AutoFillConstants.FieldIgnoreList.join(",");
|
||||||
private passwordFieldExcludeListString = AutoFillConstants.PasswordFieldExcludeList.join(",");
|
private passwordFieldExcludeListString = AutoFillConstants.PasswordFieldExcludeList.join(",");
|
||||||
private currentPasswordAutocompleteValues = new Set(["current-password"]);
|
private currentPasswordAutocompleteValue = "current-password";
|
||||||
private newPasswordAutoCompleteValues = new Set(["new-password"]);
|
private newPasswordAutoCompleteValue = "new-password";
|
||||||
private autofillFieldKeywordsMap: WeakMap<AutofillField, string> = new WeakMap();
|
private passwordAutoCompleteValues = new Set([
|
||||||
|
this.currentPasswordAutocompleteValue,
|
||||||
|
this.newPasswordAutoCompleteValue,
|
||||||
|
]);
|
||||||
|
private autofillFieldKeywordsMap: AutofillKeywordsMap = new WeakMap();
|
||||||
private autocompleteDisabledValues = new Set(["off", "false"]);
|
private autocompleteDisabledValues = new Set(["off", "false"]);
|
||||||
private newFieldKeywords = new Set(["new", "change", "neue", "ändern"]);
|
private newFieldKeywords = new Set(["new", "change", "neue", "ändern"]);
|
||||||
private accountCreationFieldKeywords = new Set([
|
private accountCreationFieldKeywords = new Set([
|
||||||
@ -26,6 +33,36 @@ export class InlineMenuFieldQualificationService
|
|||||||
"confirm",
|
"confirm",
|
||||||
...this.newFieldKeywords,
|
...this.newFieldKeywords,
|
||||||
]);
|
]);
|
||||||
|
private creditCardFieldKeywords = new Set([
|
||||||
|
...CreditCardAutoFillConstants.CardHolderFieldNames,
|
||||||
|
...CreditCardAutoFillConstants.CardNumberFieldNames,
|
||||||
|
...CreditCardAutoFillConstants.CardExpiryFieldNames,
|
||||||
|
...CreditCardAutoFillConstants.ExpiryMonthFieldNames,
|
||||||
|
...CreditCardAutoFillConstants.ExpiryYearFieldNames,
|
||||||
|
...CreditCardAutoFillConstants.CVVFieldNames,
|
||||||
|
...CreditCardAutoFillConstants.CardBrandFieldNames,
|
||||||
|
]);
|
||||||
|
private creditCardNameAutocompleteValues = new Set([
|
||||||
|
"cc-name",
|
||||||
|
"cc-given-name,",
|
||||||
|
"cc-additional-name",
|
||||||
|
"cc-family-name",
|
||||||
|
]);
|
||||||
|
private creditCardExpirationDateAutocompleteValue = "cc-exp";
|
||||||
|
private creditCardExpirationMonthAutocompleteValue = "cc-exp-month";
|
||||||
|
private creditCardExpirationYearAutocompleteValue = "cc-exp-year";
|
||||||
|
private creditCardCvvAutocompleteValue = "cc-csc";
|
||||||
|
private creditCardNumberAutocompleteValue = "cc-number";
|
||||||
|
private creditCardTypeAutocompleteValue = "cc-type";
|
||||||
|
private creditCardAutocompleteValues = new Set([
|
||||||
|
...this.creditCardNameAutocompleteValues,
|
||||||
|
this.creditCardExpirationDateAutocompleteValue,
|
||||||
|
this.creditCardExpirationMonthAutocompleteValue,
|
||||||
|
this.creditCardExpirationYearAutocompleteValue,
|
||||||
|
this.creditCardNumberAutocompleteValue,
|
||||||
|
this.creditCardCvvAutocompleteValue,
|
||||||
|
this.creditCardTypeAutocompleteValue,
|
||||||
|
]);
|
||||||
private inlineMenuFieldQualificationFlagSet = false;
|
private inlineMenuFieldQualificationFlagSet = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -59,6 +96,72 @@ export class InlineMenuFieldQualificationService
|
|||||||
return this.isUsernameFieldForLoginForm(field, pageDetails);
|
return this.isUsernameFieldForLoginForm(field, pageDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the provided field as a field for a credit card form.
|
||||||
|
*
|
||||||
|
* @param field - The field to validate
|
||||||
|
* @param pageDetails - The details of the page that the field is on.
|
||||||
|
*/
|
||||||
|
isFieldForCreditCardForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean {
|
||||||
|
// If the field contains any of the standardized autocomplete attribute values
|
||||||
|
// for credit card fields, we should assume that the field is part of a credit card form.
|
||||||
|
if (this.fieldContainsAutocompleteValues(field, this.creditCardAutocompleteValues)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the field contains any keywords indicating this is for a "new" or "changed" credit card
|
||||||
|
// field, we should assume that the field is not going to be autofilled.
|
||||||
|
if (this.keywordsFoundInFieldData(field, [...this.newFieldKeywords])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentForm = pageDetails.forms[field.form];
|
||||||
|
|
||||||
|
// If the field does not have a parent form
|
||||||
|
if (!parentForm) {
|
||||||
|
// If a credit card number field is not present on the page or there are multiple credit
|
||||||
|
// card number fields, this field is not part of a credit card form.
|
||||||
|
const numberFieldsInPageDetails = pageDetails.fields.filter(this.isFieldForCardNumber);
|
||||||
|
if (numberFieldsInPageDetails.length !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a credit card CVV field is not present on the page or there are multiple credit card
|
||||||
|
// CVV fields, this field is not part of a credit card form.
|
||||||
|
const cvvFieldsInPageDetails = pageDetails.fields.filter(this.isFieldForCardCvv);
|
||||||
|
if (cvvFieldsInPageDetails.length !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||||
|
this.keywordsFoundInFieldData(field, [...this.creditCardFieldKeywords])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the field has a parent form, check the fields from that form exclusively
|
||||||
|
const fieldsFromSameForm = pageDetails.fields.filter((f) => f.form === field.form);
|
||||||
|
|
||||||
|
// If a credit card number field is not present on the page or there are multiple credit
|
||||||
|
// card number fields, this field is not part of a credit card form.
|
||||||
|
const numberFieldsInPageDetails = fieldsFromSameForm.filter(this.isFieldForCardNumber);
|
||||||
|
if (numberFieldsInPageDetails.length !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a credit card CVV field is not present on the page or there are multiple credit card
|
||||||
|
// CVV fields, this field is not part of a credit card form.
|
||||||
|
const cvvFieldsInPageDetails = fieldsFromSameForm.filter(this.isFieldForCardCvv);
|
||||||
|
if (cvvFieldsInPageDetails.length !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||||
|
this.keywordsFoundInFieldData(field, [...this.creditCardFieldKeywords])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the provided field as a password field for a login form.
|
* Validates the provided field as a password field for a login form.
|
||||||
*
|
*
|
||||||
@ -71,12 +174,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
): boolean {
|
): boolean {
|
||||||
// If the provided field is set with an autocomplete value of "current-password", we should assume that
|
// If the provided field is set with an autocomplete value of "current-password", we should assume that
|
||||||
// the page developer intends for this field to be interpreted as a password field for a login form.
|
// the page developer intends for this field to be interpreted as a password field for a login form.
|
||||||
if (
|
if (this.fieldContainsAutocompleteValues(field, this.currentPasswordAutocompleteValue)) {
|
||||||
this.fieldContainsAutocompleteValues(
|
|
||||||
field.autoCompleteType,
|
|
||||||
this.currentPasswordAutocompleteValues,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,10 +208,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
// provided field is for a login form. This will only be the case if the field does not
|
// provided field is for a login form. This will only be the case if the field does not
|
||||||
// explicitly have its autocomplete attribute set to "off" or "false".
|
// explicitly have its autocomplete attribute set to "off" or "false".
|
||||||
|
|
||||||
return !this.fieldContainsAutocompleteValues(
|
return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||||
field.autoCompleteType,
|
|
||||||
this.autocompleteDisabledValues,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the field has a form parent and there are multiple visible password fields
|
// If the field has a form parent and there are multiple visible password fields
|
||||||
@ -135,10 +230,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
|
|
||||||
// If the field has a form parent and no username field exists and the field has an
|
// If the field has a form parent and no username field exists and the field has an
|
||||||
// autocomplete attribute set to "off" or "false", this is not a password field
|
// autocomplete attribute set to "off" or "false", this is not a password field
|
||||||
return !this.fieldContainsAutocompleteValues(
|
return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||||
field.autoCompleteType,
|
|
||||||
this.autocompleteDisabledValues,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -153,9 +245,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
): boolean {
|
): boolean {
|
||||||
// If the provided field is set with an autocomplete of "username", we should assume that
|
// If the provided field is set with an autocomplete of "username", we should assume that
|
||||||
// the page developer intends for this field to be interpreted as a username field.
|
// the page developer intends for this field to be interpreted as a username field.
|
||||||
if (
|
if (this.fieldContainsAutocompleteValues(field, this.usernameAutocompleteValues)) {
|
||||||
this.fieldContainsAutocompleteValues(field.autoCompleteType, this.usernameAutocompleteValues)
|
|
||||||
) {
|
|
||||||
const newPasswordFieldsInPageDetails = pageDetails.fields.filter(this.isNewPasswordField);
|
const newPasswordFieldsInPageDetails = pageDetails.fields.filter(this.isNewPasswordField);
|
||||||
return newPasswordFieldsInPageDetails.length === 0;
|
return newPasswordFieldsInPageDetails.length === 0;
|
||||||
}
|
}
|
||||||
@ -198,10 +288,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
// If the page does not contain any password fields, it might be part of a multistep login form.
|
// If the page does not contain any password fields, it might be part of a multistep login form.
|
||||||
// That will only be the case if the field does not explicitly have its autocomplete attribute
|
// That will only be the case if the field does not explicitly have its autocomplete attribute
|
||||||
// set to "off" or "false".
|
// set to "off" or "false".
|
||||||
return !this.fieldContainsAutocompleteValues(
|
return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||||
field.autoCompleteType,
|
|
||||||
this.autocompleteDisabledValues,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the field is structured within a form, but no password fields are present in the form,
|
// If the field is structured within a form, but no password fields are present in the form,
|
||||||
@ -209,12 +296,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
if (passwordFieldsInPageDetails.length === 0) {
|
if (passwordFieldsInPageDetails.length === 0) {
|
||||||
// If the field's autocomplete is set to a disabled value, we should assume that the field is
|
// If the field's autocomplete is set to a disabled value, we should assume that the field is
|
||||||
// not part of a login form.
|
// not part of a login form.
|
||||||
if (
|
if (this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues)) {
|
||||||
this.fieldContainsAutocompleteValues(
|
|
||||||
field.autoCompleteType,
|
|
||||||
this.autocompleteDisabledValues,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,12 +325,111 @@ export class InlineMenuFieldQualificationService
|
|||||||
|
|
||||||
// If no visible password fields are found, this field might be part of a multipart form.
|
// If no visible password fields are found, this field might be part of a multipart form.
|
||||||
// Check for an invalid autocompleteType to determine if the field is part of a login form.
|
// Check for an invalid autocompleteType to determine if the field is part of a login form.
|
||||||
return !this.fieldContainsAutocompleteValues(
|
return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||||
field.autoCompleteType,
|
|
||||||
this.autocompleteDisabledValues,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the provided field as a field for a credit card name field.
|
||||||
|
*
|
||||||
|
* @param field - The field to validate
|
||||||
|
*/
|
||||||
|
isFieldForCardholderName = (field: AutofillField): boolean => {
|
||||||
|
if (this.fieldContainsAutocompleteValues(field, this.creditCardNameAutocompleteValues)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||||
|
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CardHolderFieldNames, false)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the provided field as a field for a credit card number field.
|
||||||
|
*
|
||||||
|
* @param field - The field to validate
|
||||||
|
*/
|
||||||
|
isFieldForCardNumber = (field: AutofillField): boolean => {
|
||||||
|
if (this.fieldContainsAutocompleteValues(field, this.creditCardNumberAutocompleteValue)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||||
|
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CardNumberFieldNames, false)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the provided field as a field for a credit card expiration date field.
|
||||||
|
*
|
||||||
|
* @param field - The field to validate
|
||||||
|
*/
|
||||||
|
isFieldForCardExpirationDate = (field: AutofillField): boolean => {
|
||||||
|
if (
|
||||||
|
this.fieldContainsAutocompleteValues(field, this.creditCardExpirationDateAutocompleteValue)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||||
|
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CardExpiryFieldNames, false)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the provided field as a field for a credit card expiration month field.
|
||||||
|
*
|
||||||
|
* @param field - The field to validate
|
||||||
|
*/
|
||||||
|
isFieldForCardExpirationMonth = (field: AutofillField): boolean => {
|
||||||
|
if (
|
||||||
|
this.fieldContainsAutocompleteValues(field, this.creditCardExpirationMonthAutocompleteValue)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||||
|
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.ExpiryMonthFieldNames, false)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the provided field as a field for a credit card expiration year field.
|
||||||
|
*
|
||||||
|
* @param field - The field to validate
|
||||||
|
*/
|
||||||
|
isFieldForCardExpirationYear = (field: AutofillField): boolean => {
|
||||||
|
if (
|
||||||
|
this.fieldContainsAutocompleteValues(field, this.creditCardExpirationYearAutocompleteValue)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||||
|
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.ExpiryYearFieldNames, false)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the provided field as a field for a credit card CVV field.
|
||||||
|
*
|
||||||
|
* @param field - The field to validate
|
||||||
|
*/
|
||||||
|
isFieldForCardCvv = (field: AutofillField): boolean => {
|
||||||
|
if (this.fieldContainsAutocompleteValues(field, this.creditCardCvvAutocompleteValue)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||||
|
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CVVFieldNames, false)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the provided field as a username field.
|
* Validates the provided field as a username field.
|
||||||
*
|
*
|
||||||
@ -272,10 +453,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
*/
|
*/
|
||||||
private isCurrentPasswordField = (field: AutofillField): boolean => {
|
private isCurrentPasswordField = (field: AutofillField): boolean => {
|
||||||
if (
|
if (
|
||||||
this.fieldContainsAutocompleteValues(
|
this.fieldContainsAutocompleteValues(field, this.newPasswordAutoCompleteValue) ||
|
||||||
field.autoCompleteType,
|
|
||||||
this.newPasswordAutoCompleteValues,
|
|
||||||
) ||
|
|
||||||
this.keywordsFoundInFieldData(field, [...this.accountCreationFieldKeywords])
|
this.keywordsFoundInFieldData(field, [...this.accountCreationFieldKeywords])
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
@ -290,12 +468,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
* @param field - The field to validate
|
* @param field - The field to validate
|
||||||
*/
|
*/
|
||||||
private isNewPasswordField = (field: AutofillField): boolean => {
|
private isNewPasswordField = (field: AutofillField): boolean => {
|
||||||
if (
|
if (this.fieldContainsAutocompleteValues(field, this.currentPasswordAutocompleteValue)) {
|
||||||
this.fieldContainsAutocompleteValues(
|
|
||||||
field.autoCompleteType,
|
|
||||||
this.currentPasswordAutocompleteValues,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,23 +605,33 @@ export class InlineMenuFieldQualificationService
|
|||||||
*
|
*
|
||||||
* @param autofillFieldData - The field data to search for keywords
|
* @param autofillFieldData - The field data to search for keywords
|
||||||
* @param keywords - The keywords to search for
|
* @param keywords - The keywords to search for
|
||||||
|
* @param fuzzyMatchKeywords - Indicates if the keywords should be matched in a fuzzy manner
|
||||||
*/
|
*/
|
||||||
private keywordsFoundInFieldData(autofillFieldData: AutofillField, keywords: string[]) {
|
private keywordsFoundInFieldData(
|
||||||
const searchedString = this.getAutofillFieldDataKeywords(autofillFieldData);
|
autofillFieldData: AutofillField,
|
||||||
return keywords.some((keyword) => searchedString.includes(keyword));
|
keywords: string[],
|
||||||
|
fuzzyMatchKeywords = true,
|
||||||
|
) {
|
||||||
|
const searchedValues = this.getAutofillFieldDataKeywords(autofillFieldData, fuzzyMatchKeywords);
|
||||||
|
if (typeof searchedValues === "string") {
|
||||||
|
return keywords.some((keyword) => searchedValues.indexOf(keyword) > -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keywords.some((keyword) => searchedValues.has(keyword));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the keywords from the provided autofill field data.
|
* Retrieves the keywords from the provided autofill field data.
|
||||||
*
|
*
|
||||||
* @param autofillFieldData - The field data to search for keywords
|
* @param autofillFieldData - The field data to search for keywords
|
||||||
|
* @param returnStringValue - Indicates if the method should return a string value
|
||||||
*/
|
*/
|
||||||
private getAutofillFieldDataKeywords(autofillFieldData: AutofillField) {
|
private getAutofillFieldDataKeywords(
|
||||||
if (this.autofillFieldKeywordsMap.has(autofillFieldData)) {
|
autofillFieldData: AutofillField,
|
||||||
return this.autofillFieldKeywordsMap.get(autofillFieldData);
|
returnStringValue: boolean,
|
||||||
}
|
) {
|
||||||
|
if (!this.autofillFieldKeywordsMap.has(autofillFieldData)) {
|
||||||
const keywordValues = [
|
const keywords = [
|
||||||
autofillFieldData.htmlID,
|
autofillFieldData.htmlID,
|
||||||
autofillFieldData.htmlName,
|
autofillFieldData.htmlName,
|
||||||
autofillFieldData.htmlClass,
|
autofillFieldData.htmlClass,
|
||||||
@ -462,30 +645,37 @@ export class InlineMenuFieldQualificationService
|
|||||||
autofillFieldData["label-right"],
|
autofillFieldData["label-right"],
|
||||||
autofillFieldData["label-tag"],
|
autofillFieldData["label-tag"],
|
||||||
autofillFieldData["label-top"],
|
autofillFieldData["label-top"],
|
||||||
]
|
];
|
||||||
.join(",")
|
const keywordsSet = new Set<string>(keywords);
|
||||||
.toLowerCase();
|
const stringValue = keywords.join(",").toLowerCase();
|
||||||
this.autofillFieldKeywordsMap.set(autofillFieldData, keywordValues);
|
this.autofillFieldKeywordsMap.set(autofillFieldData, { keywordsSet, stringValue });
|
||||||
|
}
|
||||||
|
|
||||||
return keywordValues;
|
const mapValues = this.autofillFieldKeywordsMap.get(autofillFieldData);
|
||||||
|
return returnStringValue ? mapValues.stringValue : mapValues.keywordsSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Separates the provided field data into space-separated values and checks if any
|
* Separates the provided field data into space-separated values and checks if any
|
||||||
* of the values are present in the provided set of autocomplete values.
|
* of the values are present in the provided set of autocomplete values.
|
||||||
*
|
*
|
||||||
* @param fieldAutocompleteValue - The field autocomplete value to validate
|
* @param autofillFieldData - The field autocomplete value to validate
|
||||||
* @param compareValues - The set of autocomplete values to check against
|
* @param compareValues - The set of autocomplete values to check against
|
||||||
*/
|
*/
|
||||||
private fieldContainsAutocompleteValues(
|
private fieldContainsAutocompleteValues(
|
||||||
fieldAutocompleteValue: string,
|
autofillFieldData: AutofillField,
|
||||||
compareValues: Set<string>,
|
compareValues: string | Set<string>,
|
||||||
) {
|
) {
|
||||||
if (!fieldAutocompleteValue) {
|
const fieldAutocompleteValue = autofillFieldData.autoCompleteType;
|
||||||
|
if (!fieldAutocompleteValue || typeof fieldAutocompleteValue !== "string") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const autocompleteValueParts = fieldAutocompleteValue.split(" ");
|
const autocompleteValueParts = fieldAutocompleteValue.split(" ");
|
||||||
|
if (typeof compareValues === "string") {
|
||||||
|
return autocompleteValueParts.indexOf(compareValues) > -1;
|
||||||
|
}
|
||||||
|
|
||||||
for (let index = 0; index < autocompleteValueParts.length; index++) {
|
for (let index = 0; index < autocompleteValueParts.length; index++) {
|
||||||
if (compareValues.has(autocompleteValueParts[index])) {
|
if (compareValues.has(autocompleteValueParts[index])) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -208,6 +208,7 @@ export function createInitAutofillInlineMenuListMessageMock(
|
|||||||
theme: ThemeType.Light,
|
theme: ThemeType.Light,
|
||||||
authStatus: AuthenticationStatus.Unlocked,
|
authStatus: AuthenticationStatus.Unlocked,
|
||||||
portKey: "portKey",
|
portKey: "portKey",
|
||||||
|
filledByCipherType: CipherType.Login,
|
||||||
ciphers: [
|
ciphers: [
|
||||||
createAutofillOverlayCipherDataMock(1, {
|
createAutofillOverlayCipherDataMock(1, {
|
||||||
icon: {
|
icon: {
|
||||||
@ -254,6 +255,7 @@ export function createFocusedFieldDataMock(customFields = {}) {
|
|||||||
paddingRight: "6px",
|
paddingRight: "6px",
|
||||||
paddingLeft: "6px",
|
paddingLeft: "6px",
|
||||||
},
|
},
|
||||||
|
filledByCipherType: CipherType.Login,
|
||||||
tabId: 1,
|
tabId: 1,
|
||||||
frameId: 2,
|
frameId: 2,
|
||||||
...customFields,
|
...customFields,
|
||||||
|
@ -141,6 +141,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
params.uri &&
|
params.uri &&
|
||||||
|
this.cipher.login.uris[0] &&
|
||||||
(this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === "")
|
(this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === "")
|
||||||
) {
|
) {
|
||||||
this.cipher.login.uris[0].uri = params.uri;
|
this.cipher.login.uris[0].uri = params.uri;
|
||||||
|
Loading…
Reference in New Issue
Block a user