mirror of
https://github.com/bitwarden/browser.git
synced 2025-03-12 13:39:14 +01:00
Merge branch 'main' into autofill/pm-6546-blurring-of-autofilled-elements-causes-problems-in-blur-event-listeners
This commit is contained in:
commit
48df76b2d7
21
.github/workflows/build-browser.yml
vendored
21
.github/workflows/build-browser.yml
vendored
@ -394,25 +394,8 @@ jobs:
|
||||
- crowdin-push
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
if: ${{ (github.ref == 'refs/heads/main') || (github.ref == 'refs/heads/rc') }}
|
||||
env:
|
||||
SETUP_STATUS: ${{ needs.setup.result }}
|
||||
LOCALES_TEST_STATUS: ${{ needs.locales-test.result }}
|
||||
BUILD_STATUS: ${{ needs.build.result }}
|
||||
SAFARI_BUILD_STATUS: ${{ needs.build-safari.result }}
|
||||
CROWDIN_PUSH_STATUS: ${{ needs.crowdin-push.result }}
|
||||
run: |
|
||||
if [ "$SETUP_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$LOCALES_TEST_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$BUILD_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$SAFARI_BUILD_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$CROWDIN_PUSH_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
fi
|
||||
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
- name: Login to Azure - Prod Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
|
18
.github/workflows/build-cli.yml
vendored
18
.github/workflows/build-cli.yml
vendored
@ -278,6 +278,9 @@ jobs:
|
||||
|
||||
snap:
|
||||
name: Build Snap
|
||||
# Note, before updating the ubuntu version of the workflow, ensure the snap base image
|
||||
# is equal or greater than the new version. Otherwise there might be GLIBC version issues.
|
||||
# The snap base for CLI is defined in `apps/cli/stores/snap/snapcraft.yaml`
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [setup, cli]
|
||||
env:
|
||||
@ -362,19 +365,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
working-directory: ${{ github.workspace }}
|
||||
if: ${{ (github.ref == 'refs/heads/main') || (github.ref == 'refs/heads/rc') }}
|
||||
env:
|
||||
SETUP_STATUS: ${{ needs.setup.result }}
|
||||
CLI_STATUS: ${{ needs.cli.result }}
|
||||
SNAP_STATUS: ${{ needs.snap.result }}
|
||||
run: |
|
||||
if [ "$SETUP_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$CLI_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$SNAP_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
fi
|
||||
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
- name: Login to Azure - Prod Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
|
36
.github/workflows/build-desktop.yml
vendored
36
.github/workflows/build-desktop.yml
vendored
@ -124,6 +124,9 @@ jobs:
|
||||
|
||||
linux:
|
||||
name: Linux Build
|
||||
# Note, before updating the ubuntu version of the workflow, ensure the snap base image
|
||||
# is equal or greater than the new version. Otherwise there might be GLIBC version issues.
|
||||
# The snap base for desktop is defined in `apps/desktop/electron-builder.json`
|
||||
runs-on: ubuntu-22.04
|
||||
needs: setup
|
||||
env:
|
||||
@ -1236,37 +1239,8 @@ jobs:
|
||||
- crowdin-push
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
if: ${{ (github.ref == 'refs/heads/main') || (github.ref == 'refs/heads/rc') }}
|
||||
env:
|
||||
ELECTRON_VERIFY_STATUS: ${{ needs.electron-verify.result }}
|
||||
BROWSER_BUILD_STATUS: ${{ needs.browser-build.result }}
|
||||
SETUP_STATUS: ${{ needs.setup.result }}
|
||||
LINUX_STATUS: ${{ needs.linux.result }}
|
||||
WINDOWS_STATUS: ${{ needs.windows.result }}
|
||||
MACOS_BUILD_STATUS: ${{ needs.macos-build.result }}
|
||||
MACOS_PKG_GITHUB_STATUS: ${{ needs.macos-package-github.result }}
|
||||
MACOS_PKG_MAS_STATUS: ${{ needs.macos-package-mas.result }}
|
||||
CROWDIN_PUSH_STATUS: ${{ needs.crowdin-push.result }}
|
||||
run: |
|
||||
if [ "$ELECTRON_VERIFY_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$BROWSER_BUILD_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$SETUP_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$LINUX_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$WINDOWS_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$MACOS_BUILD_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$MACOS_PKG_GITHUB_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$MACOS_PKG_MAS_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$CROWDIN_PUSH_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
fi
|
||||
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
- name: Login to Azure - Prod Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
|
23
.github/workflows/build-web.yml
vendored
23
.github/workflows/build-web.yml
vendored
@ -327,27 +327,8 @@ jobs:
|
||||
- trigger-web-vault-deploy
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
if: ${{ (github.ref == 'refs/heads/main') || (github.ref == 'refs/heads/rc') }}
|
||||
env:
|
||||
SETUP_STATUS: ${{ needs.setup.result }}
|
||||
ARTIFACT_STATUS: ${{ needs.build-artifacts.result }}
|
||||
BUILD_CONTAINERS_STATUS: ${{ needs.build-containers.result }}
|
||||
CROWDIN_PUSH_STATUS: ${{ needs.crowdin-push.result }}
|
||||
TRIGGER_WEB_VAULT_DEPLOY_STATUS: ${{ needs.trigger-web-vault-deploy.result }}
|
||||
run: |
|
||||
if [ "$SETUP_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$ARTIFACT_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$BUILD_SELFHOST_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$BUILD_CONTAINERS_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$CROWDIN_PUSH_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$TRIGGER_WEB_VAULT_DEPLOY_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
fi
|
||||
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
- name: Login to Azure - Prod Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
|
32
.github/workflows/deploy-web.yml
vendored
32
.github/workflows/deploy-web.yml
vendored
@ -19,6 +19,10 @@ on:
|
||||
description: "Branch or Tag name to deploy (examples: 'main', 'feature/sm', 'web-v2023.12.0')"
|
||||
type: string
|
||||
default: main
|
||||
invert-default-sync-delete-destination-files-value:
|
||||
description: "Invert the default sync-delete-destination-files value"
|
||||
type: boolean
|
||||
default: false
|
||||
debug:
|
||||
description: "Debug mode"
|
||||
type: boolean
|
||||
@ -34,10 +38,14 @@ on:
|
||||
description: "Branch or Tag name to deploy (examples: 'main', 'feature/sm', 'web-v2023.12.0')"
|
||||
type: string
|
||||
default: main
|
||||
invert-default-sync-delete-destination-files-value:
|
||||
description: "Invert the default sync-delete-destination-files value"
|
||||
type: boolean
|
||||
default: false
|
||||
debug:
|
||||
description: "Debug mode"
|
||||
type: boolean
|
||||
default: false
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
deployments: write
|
||||
@ -54,6 +62,7 @@ jobs:
|
||||
azure-login-creds: ${{ steps.config.outputs.azure-login-creds }}
|
||||
retrieve-secrets-keyvault: ${{ steps.config.outputs.retrieve-secrets-keyvault }}
|
||||
sync-utility: ${{ steps.config.outputs.sync-utility }}
|
||||
sync-delete-destination-files: ${{ steps.config.outputs.sync-delete-destination-files }}
|
||||
steps:
|
||||
- name: Configure
|
||||
id: config
|
||||
@ -61,6 +70,15 @@ jobs:
|
||||
ENV_NAME_LOWER=$(echo "${{ inputs.environment }}" | awk '{print tolower($0)}')
|
||||
echo "configuring the Web deploy for ${{ inputs.environment }}"
|
||||
echo "environment=${{ inputs.environment }}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Invert the default value for sync-delete-destination-files
|
||||
if [ ${{ inputs.invert-default-sync-delete-destination-files-value }} ]; then
|
||||
echo "sync-delete-destination-files=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
# This is the default value for USQA, EUQA, USPROD, and EUPROD
|
||||
echo "sync-delete-destination-files=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
case ${{ inputs.environment }} in
|
||||
"USQA")
|
||||
echo "azure-login-creds=AZURE_KV_US_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
|
||||
@ -96,6 +114,13 @@ jobs:
|
||||
echo "environment-artifact=web-*-cloud-usdev.zip" >> $GITHUB_OUTPUT
|
||||
echo "environment-name=Web Vault - US Development Cloud" >> $GITHUB_OUTPUT
|
||||
echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ ${{ inputs.invert-default-sync-delete-destination-files-value }} ]; then
|
||||
echo "sync-delete-destination-files=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
# This is the default value for USDEV
|
||||
echo "sync-delete-destination-files=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
# Set the sync utility to use for deployment to the environment (az-sync or azcopy)
|
||||
@ -259,7 +284,8 @@ jobs:
|
||||
az storage blob sync \
|
||||
--source "./build" \
|
||||
--container '$web' \
|
||||
--connection-string "${{ steps.retrieve-secrets-az-sync.outputs.sa-bitwarden-web-vault-dev-key-temp }}"
|
||||
--connection-string "${{ steps.retrieve-secrets-az-sync.outputs.sa-bitwarden-web-vault-dev-key-temp }}" \
|
||||
--delete-destination=${{ needs.setup.outputs.sync-delete-destination-files }}
|
||||
|
||||
- name: Sync to Azure Storage Account using azcopy
|
||||
if: ${{ needs.setup.outputs.sync-utility == 'azcopy' }}
|
||||
@ -271,7 +297,7 @@ jobs:
|
||||
AZCOPY_TENANT_ID: ${{ steps.retrieve-secrets-azcopy.outputs.sp-bitwarden-web-vault-tenant }}
|
||||
run: |
|
||||
azcopy sync ./build 'https://${{ steps.retrieve-secrets-azcopy.outputs.sa-bitwarden-web-vault-name }}.blob.core.windows.net/$web/' \
|
||||
--delete-destination=true
|
||||
--delete-destination=${{ needs.setup.outputs.sync-delete-destination-files }} --compare-hash="MD5"
|
||||
|
||||
- name: Debug sync logs
|
||||
if: ${{ inputs.debug }}
|
||||
|
@ -1259,7 +1259,7 @@ describe("OverlayBackground", () => {
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code", { window });
|
||||
expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code");
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -241,7 +241,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
});
|
||||
|
||||
if (totpCode) {
|
||||
this.platformUtilsService.copyToClipboard(totpCode, { window });
|
||||
this.platformUtilsService.copyToClipboard(totpCode);
|
||||
}
|
||||
|
||||
this.overlayLoginCiphers = new Map([[overlayCipherId, cipher], ...this.overlayLoginCiphers]);
|
||||
|
@ -16,7 +16,7 @@ export default class WebRequestBackground {
|
||||
private cipherService: CipherService,
|
||||
private authService: AuthService,
|
||||
) {
|
||||
if (BrowserApi.manifestVersion === 2) {
|
||||
if (BrowserApi.isManifestVersion(2)) {
|
||||
this.webRequest = (window as any).chrome.webRequest;
|
||||
}
|
||||
this.isFirefox = platformUtilsService.isFirefox();
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
|
||||
|
||||
import { postWindowMessage, sendExtensionRuntimeMessage } from "../spec/testing-utils";
|
||||
|
||||
describe("ContentMessageHandler", () => {
|
||||
@ -30,9 +32,11 @@ describe("ContentMessageHandler", () => {
|
||||
const mockPostMessage = jest.fn();
|
||||
window.postMessage = mockPostMessage;
|
||||
|
||||
postWindowMessage({ command: "checkIfBWExtensionInstalled" });
|
||||
postWindowMessage({ command: VaultOnboardingMessages.checkBwInstalled });
|
||||
|
||||
expect(mockPostMessage).toHaveBeenCalled();
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
command: VaultOnboardingMessages.HasBwInstalled,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
|
||||
|
||||
import {
|
||||
ContentMessageWindowData,
|
||||
ContentMessageWindowEventHandlers,
|
||||
@ -33,7 +35,7 @@ const windowMessageHandlers: ContentMessageWindowEventHandlers = {
|
||||
* Handles the post to the web vault showing the extension has been installed
|
||||
*/
|
||||
function handleExtensionInstallCheck() {
|
||||
window.postMessage({ command: "hasBWInstalled" });
|
||||
window.postMessage({ command: VaultOnboardingMessages.HasBwInstalled });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,7 +64,7 @@ export default class CommandsBackground {
|
||||
private async generatePasswordToClipboard() {
|
||||
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
|
||||
const password = await this.passwordGenerationService.generatePassword(options);
|
||||
this.platformUtilsService.copyToClipboard(password, { window: window });
|
||||
this.platformUtilsService.copyToClipboard(password);
|
||||
await this.passwordGenerationService.addHistory(password);
|
||||
}
|
||||
|
||||
|
@ -325,7 +325,7 @@ export default class MainBackground {
|
||||
popupOnlyContext: boolean;
|
||||
|
||||
constructor(public isPrivateMode: boolean = false) {
|
||||
this.popupOnlyContext = isPrivateMode || BrowserApi.manifestVersion === 3;
|
||||
this.popupOnlyContext = isPrivateMode || BrowserApi.isManifestVersion(3);
|
||||
|
||||
// Services
|
||||
const lockedCallback = async (userId?: string) => {
|
||||
@ -353,15 +353,13 @@ export default class MainBackground {
|
||||
this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService);
|
||||
this.storageService = new BrowserLocalStorageService();
|
||||
this.secureStorageService = new BrowserLocalStorageService();
|
||||
this.memoryStorageService =
|
||||
BrowserApi.manifestVersion === 3
|
||||
this.memoryStorageService = BrowserApi.isManifestVersion(3)
|
||||
? new LocalBackedSessionStorageService(
|
||||
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
|
||||
this.keyGenerationService,
|
||||
)
|
||||
: new MemoryStorageService();
|
||||
this.memoryStorageForStateProviders =
|
||||
BrowserApi.manifestVersion === 3
|
||||
this.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3)
|
||||
? new LocalBackedSessionStorageService(
|
||||
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
|
||||
this.keyGenerationService,
|
||||
@ -462,7 +460,7 @@ export default class MainBackground {
|
||||
return promise.then((result) => result.response === "unlocked");
|
||||
}
|
||||
},
|
||||
window,
|
||||
self,
|
||||
);
|
||||
this.i18nService = new BrowserI18nService(BrowserApi.getUILanguage(), this.stateService);
|
||||
this.cryptoService = new BrowserCryptoService(
|
||||
@ -898,11 +896,11 @@ export default class MainBackground {
|
||||
);
|
||||
if (!this.popupOnlyContext) {
|
||||
const contextMenuClickedHandler = new ContextMenuClickedHandler(
|
||||
(options) => this.platformUtilsService.copyToClipboard(options.text, { window: self }),
|
||||
(options) => this.platformUtilsService.copyToClipboard(options.text),
|
||||
async (_tab) => {
|
||||
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
|
||||
const password = await this.passwordGenerationService.generatePassword(options);
|
||||
this.platformUtilsService.copyToClipboard(password, { window: window });
|
||||
this.platformUtilsService.copyToClipboard(password);
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.passwordGenerationService.addHistory(password);
|
||||
@ -1143,7 +1141,7 @@ export default class MainBackground {
|
||||
await this.reseedStorage();
|
||||
}
|
||||
|
||||
if (BrowserApi.manifestVersion === 3) {
|
||||
if (BrowserApi.isManifestVersion(3)) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
BrowserApi.sendMessage("updateBadge");
|
||||
|
@ -172,7 +172,7 @@ export default class RuntimeBackground {
|
||||
msg.sender === "autofill_cmd",
|
||||
);
|
||||
if (totpCode != null) {
|
||||
this.platformUtilsService.copyToClipboard(totpCode, { window: window });
|
||||
this.platformUtilsService.copyToClipboard(totpCode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -261,7 +261,7 @@ export default class RuntimeBackground {
|
||||
});
|
||||
break;
|
||||
case "getClickedElementResponse":
|
||||
this.platformUtilsService.copyToClipboard(msg.identifier, { window: window });
|
||||
this.platformUtilsService.copyToClipboard(msg.identifier);
|
||||
break;
|
||||
case "triggerFido2ContentScriptInjection":
|
||||
await this.fido2Service.injectFido2ContentScripts(sender);
|
||||
@ -319,7 +319,7 @@ export default class RuntimeBackground {
|
||||
});
|
||||
|
||||
if (totpCode != null) {
|
||||
this.platformUtilsService.copyToClipboard(totpCode, { window: window });
|
||||
this.platformUtilsService.copyToClipboard(totpCode);
|
||||
}
|
||||
|
||||
// reset
|
||||
@ -345,10 +345,34 @@ export default class RuntimeBackground {
|
||||
if (await this.environmentService.hasManagedEnvironment()) {
|
||||
await this.environmentService.setUrlsToManagedEnvironment();
|
||||
}
|
||||
|
||||
await this.sendBwInstalledMessageToVault();
|
||||
}
|
||||
|
||||
this.onInstalledReason = null;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async sendBwInstalledMessageToVault() {
|
||||
try {
|
||||
const vaultUrl = this.environmentService.getWebVaultUrl();
|
||||
const urlObj = new URL(vaultUrl);
|
||||
|
||||
const tabs = await BrowserApi.tabsQuery({ url: `${urlObj.href}*` });
|
||||
|
||||
if (!tabs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const tab of tabs) {
|
||||
await BrowserApi.executeScriptInTab(tab.id, {
|
||||
file: "content/send-on-installed-message.js",
|
||||
runAt: "document_end",
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(`Error sending on installed message to vault: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,8 @@
|
||||
"clipboardWrite",
|
||||
"idle",
|
||||
"alarms",
|
||||
"scripting"
|
||||
"scripting",
|
||||
"offscreen"
|
||||
],
|
||||
"optional_permissions": ["nativeMessaging", "privacy"],
|
||||
"host_permissions": ["http://*/*", "https://*/*"],
|
||||
|
@ -22,7 +22,7 @@ const alarmState: AlarmState = {
|
||||
*/
|
||||
export async function getAlarmTime(commandName: AlarmKeys): Promise<number> {
|
||||
let alarmTime: number;
|
||||
if (BrowserApi.manifestVersion == 3) {
|
||||
if (BrowserApi.isManifestVersion(3)) {
|
||||
const fromSessionStore = await chrome.storage.session.get(commandName);
|
||||
alarmTime = fromSessionStore[commandName];
|
||||
} else {
|
||||
@ -58,7 +58,7 @@ export async function clearAlarmTime(commandName: AlarmKeys): Promise<void> {
|
||||
}
|
||||
|
||||
async function setAlarmTimeInternal(commandName: AlarmKeys, time: number): Promise<void> {
|
||||
if (BrowserApi.manifestVersion == 3) {
|
||||
if (BrowserApi.isManifestVersion(3)) {
|
||||
await chrome.storage.session.set({ [commandName]: time });
|
||||
} else {
|
||||
alarmState[commandName] = time;
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
tabsOnUpdatedListener,
|
||||
} from "./listeners";
|
||||
|
||||
if (BrowserApi.manifestVersion === 3) {
|
||||
if (BrowserApi.isManifestVersion(3)) {
|
||||
chrome.commands.onCommand.addListener(onCommandListener);
|
||||
chrome.runtime.onInstalled.addListener(onInstallListener);
|
||||
chrome.alarms.onAlarm.addListener(onAlarmListener);
|
||||
|
@ -52,7 +52,7 @@ export function memoryStorageServiceFactory(
|
||||
opts: MemoryStorageServiceInitOptions,
|
||||
): Promise<AbstractMemoryStorageService> {
|
||||
return factory(cache, "memoryStorageService", opts, async () => {
|
||||
if (BrowserApi.manifestVersion === 3) {
|
||||
if (BrowserApi.isManifestVersion(3)) {
|
||||
return new LocalBackedSessionStorageService(
|
||||
await encryptServiceFactory(cache, opts),
|
||||
await keyGenerationServiceFactory(cache, opts),
|
||||
|
@ -9,6 +9,24 @@ describe("BrowserApi", () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("isManifestVersion", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
|
||||
});
|
||||
|
||||
it("returns true if the manifest version matches the provided version", () => {
|
||||
const result = BrowserApi.isManifestVersion(3);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false if the manifest version does not match the provided version", () => {
|
||||
const result = BrowserApi.isManifestVersion(2);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWindow", () => {
|
||||
it("will get the current window if a window id is not provided", () => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
@ -106,6 +124,38 @@ describe("BrowserApi", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTab", () => {
|
||||
it("returns `null` if the tabId is a falsy value", async () => {
|
||||
const result = await BrowserApi.getTab(null);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns the tab within manifest v3", async () => {
|
||||
const tabId = 1;
|
||||
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
|
||||
(chrome.tabs.get as jest.Mock).mockImplementation(
|
||||
(tabId) => ({ id: tabId }) as chrome.tabs.Tab,
|
||||
);
|
||||
|
||||
const result = await BrowserApi.getTab(tabId);
|
||||
|
||||
expect(result).toEqual({ id: tabId });
|
||||
});
|
||||
|
||||
it("returns the tab within manifest v2", async () => {
|
||||
const tabId = 1;
|
||||
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(2);
|
||||
(chrome.tabs.get as jest.Mock).mockImplementation((tabId, callback) =>
|
||||
callback({ id: tabId } as chrome.tabs.Tab),
|
||||
);
|
||||
|
||||
const result = BrowserApi.getTab(tabId);
|
||||
|
||||
await expect(result).resolves.toEqual({ id: tabId });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBackgroundPage", () => {
|
||||
it("returns a null value if the `getBackgroundPage` method is not available", () => {
|
||||
chrome.extension.getBackgroundPage = undefined;
|
||||
@ -280,6 +330,24 @@ describe("BrowserApi", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBrowserAction", () => {
|
||||
it("returns the `chrome.action` API if the extension manifest is for version 3", () => {
|
||||
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
|
||||
|
||||
const result = BrowserApi.getBrowserAction();
|
||||
|
||||
expect(result).toEqual(chrome.action);
|
||||
});
|
||||
|
||||
it("returns the `chrome.browserAction` API if the extension manifest is for version 2", () => {
|
||||
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(2);
|
||||
|
||||
const result = BrowserApi.getBrowserAction();
|
||||
|
||||
expect(result).toEqual(chrome.browserAction);
|
||||
});
|
||||
});
|
||||
|
||||
describe("executeScriptInTab", () => {
|
||||
it("calls to the extension api to execute a script within the give tabId", async () => {
|
||||
const tabId = 1;
|
||||
@ -456,4 +524,30 @@ describe("BrowserApi", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createOffscreenDocument", () => {
|
||||
it("creates the offscreen document with the supplied reasons and justification", async () => {
|
||||
const reasons = [chrome.offscreen.Reason.CLIPBOARD];
|
||||
const justification = "justification";
|
||||
|
||||
await BrowserApi.createOffscreenDocument(reasons, justification);
|
||||
|
||||
expect(chrome.offscreen.createDocument).toHaveBeenCalledWith({
|
||||
url: "offscreen-document/index.html",
|
||||
reasons,
|
||||
justification,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("closeOffscreenDocument", () => {
|
||||
it("closes the offscreen document", () => {
|
||||
const callbackMock = jest.fn();
|
||||
|
||||
BrowserApi.closeOffscreenDocument(callbackMock);
|
||||
|
||||
expect(chrome.offscreen.closeDocument).toHaveBeenCalled();
|
||||
expect(callbackMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -19,6 +19,15 @@ export class BrowserApi {
|
||||
return chrome.runtime.getManifest().manifest_version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the extension manifest version is the given version.
|
||||
*
|
||||
* @param expectedVersion - The expected manifest version to check against.
|
||||
*/
|
||||
static isManifestVersion(expectedVersion: 2 | 3) {
|
||||
return BrowserApi.manifestVersion === expectedVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current window or the window with the given id.
|
||||
*
|
||||
@ -98,12 +107,17 @@ export class BrowserApi {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tab with the given id.
|
||||
*
|
||||
* @param tabId - The id of the tab to get.
|
||||
*/
|
||||
static async getTab(tabId: number): Promise<chrome.tabs.Tab> | null {
|
||||
if (!tabId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (BrowserApi.manifestVersion === 3) {
|
||||
if (BrowserApi.isManifestVersion(3)) {
|
||||
return await chrome.tabs.get(tabId);
|
||||
}
|
||||
|
||||
@ -453,8 +467,11 @@ export class BrowserApi {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the supported BrowserAction API based on the manifest version.
|
||||
*/
|
||||
static getBrowserAction() {
|
||||
return BrowserApi.manifestVersion === 3 ? chrome.action : chrome.browserAction;
|
||||
return BrowserApi.isManifestVersion(3) ? chrome.action : chrome.browserAction;
|
||||
}
|
||||
|
||||
static getSidebarAction(
|
||||
@ -488,7 +505,7 @@ export class BrowserApi {
|
||||
world: chrome.scripting.ExecutionWorld;
|
||||
},
|
||||
): Promise<unknown> {
|
||||
if (BrowserApi.manifestVersion === 3) {
|
||||
if (BrowserApi.isManifestVersion(3)) {
|
||||
return chrome.scripting.executeScript({
|
||||
target: {
|
||||
tabId: tabId,
|
||||
@ -546,4 +563,32 @@ export class BrowserApi {
|
||||
chrome.privacy.services.autofillCreditCardEnabled.set({ value });
|
||||
chrome.privacy.services.passwordSavingEnabled.set({ value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the offscreen document with the given reasons and justification.
|
||||
*
|
||||
* @param reasons - List of reasons for opening the offscreen document.
|
||||
* @see https://developer.chrome.com/docs/extensions/reference/api/offscreen#type-Reason
|
||||
* @param justification - Custom written justification for opening the offscreen document.
|
||||
*/
|
||||
static async createOffscreenDocument(reasons: chrome.offscreen.Reason[], justification: string) {
|
||||
await chrome.offscreen.createDocument({
|
||||
url: "offscreen-document/index.html",
|
||||
reasons,
|
||||
justification,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the offscreen document.
|
||||
*
|
||||
* @param callback - Optional callback to execute after the offscreen document is closed.
|
||||
*/
|
||||
static closeOffscreenDocument(callback?: () => void) {
|
||||
chrome.offscreen.closeDocument(() => {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ export class SessionSyncer {
|
||||
|
||||
async update(serializedValue: any) {
|
||||
const unBuiltValue = JSON.parse(serializedValue);
|
||||
if (BrowserApi.manifestVersion !== 3 && BrowserApi.isBackgroundPage(self)) {
|
||||
if (!BrowserApi.isManifestVersion(3) && BrowserApi.isBackgroundPage(self)) {
|
||||
await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue);
|
||||
}
|
||||
const builder = SyncedItemMetadata.builder(this.metaData);
|
||||
@ -105,7 +105,7 @@ export class SessionSyncer {
|
||||
|
||||
private async updateSession(value: any) {
|
||||
const serializedValue = JSON.stringify(value);
|
||||
if (BrowserApi.manifestVersion === 3 || BrowserApi.isBackgroundPage(self)) {
|
||||
if (BrowserApi.isManifestVersion(3) || BrowserApi.isBackgroundPage(self)) {
|
||||
await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue);
|
||||
}
|
||||
await BrowserApi.sendMessage(this.updateMessageCommand, { id: this.id, serializedValue });
|
||||
|
6
apps/browser/src/platform/globals.d.ts
vendored
6
apps/browser/src/platform/globals.d.ts
vendored
@ -117,6 +117,12 @@ interface Window {
|
||||
opera: unknown;
|
||||
}
|
||||
|
||||
interface ServiceWorkerGlobalScope {
|
||||
chrome?: typeof chrome;
|
||||
opr?: Opera | undefined;
|
||||
opera?: unknown;
|
||||
}
|
||||
|
||||
declare let opr: Opera | undefined;
|
||||
declare let opera: unknown | undefined;
|
||||
declare let safari: any;
|
||||
|
@ -0,0 +1,26 @@
|
||||
type OffscreenDocumentExtensionMessage = {
|
||||
[key: string]: any;
|
||||
command: string;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
type OffscreenExtensionMessageEventParams = {
|
||||
message: OffscreenDocumentExtensionMessage;
|
||||
sender: chrome.runtime.MessageSender;
|
||||
};
|
||||
|
||||
type OffscreenDocumentExtensionMessageHandlers = {
|
||||
[key: string]: ({ message, sender }: OffscreenExtensionMessageEventParams) => any;
|
||||
offscreenCopyToClipboard: ({ message }: OffscreenExtensionMessageEventParams) => any;
|
||||
offscreenReadFromClipboard: () => any;
|
||||
};
|
||||
|
||||
interface OffscreenDocument {
|
||||
init(): void;
|
||||
}
|
||||
|
||||
export {
|
||||
OffscreenDocumentExtensionMessage,
|
||||
OffscreenDocumentExtensionMessageHandlers,
|
||||
OffscreenDocument,
|
||||
};
|
13
apps/browser/src/platform/offscreen-document/index.html
Normal file
13
apps/browser/src/platform/offscreen-document/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
|
||||
/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>Bitwarden Offscreen Document</title>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
@ -0,0 +1,62 @@
|
||||
import { flushPromises, sendExtensionRuntimeMessage } from "../../autofill/spec/testing-utils";
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
import BrowserClipboardService from "../services/browser-clipboard.service";
|
||||
|
||||
describe("OffscreenDocument", () => {
|
||||
const browserApiMessageListenerSpy = jest.spyOn(BrowserApi, "messageListener");
|
||||
const browserClipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy");
|
||||
const browserClipboardServiceReadSpy = jest.spyOn(BrowserClipboardService, "read");
|
||||
const consoleErrorSpy = jest.spyOn(console, "error");
|
||||
|
||||
require("../offscreen-document/offscreen-document");
|
||||
|
||||
describe("init", () => {
|
||||
it("sets up a `chrome.runtime.onMessage` listener", () => {
|
||||
expect(browserApiMessageListenerSpy).toHaveBeenCalledWith(
|
||||
"offscreen-document",
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extension message handlers", () => {
|
||||
it("ignores messages that do not have a handler registered with the corresponding command", () => {
|
||||
sendExtensionRuntimeMessage({ command: "notAValidCommand" });
|
||||
|
||||
expect(browserClipboardServiceCopySpy).not.toHaveBeenCalled();
|
||||
expect(browserClipboardServiceReadSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows a console message if the handler throws an error", async () => {
|
||||
browserClipboardServiceCopySpy.mockRejectedValueOnce(new Error("test error"));
|
||||
|
||||
sendExtensionRuntimeMessage({ command: "offscreenCopyToClipboard", text: "test" });
|
||||
await flushPromises();
|
||||
|
||||
expect(browserClipboardServiceCopySpy).toHaveBeenCalled();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"Error resolving extension message response: Error: test error",
|
||||
);
|
||||
});
|
||||
|
||||
describe("handleOffscreenCopyToClipboard", () => {
|
||||
it("copies the message text", async () => {
|
||||
const text = "test";
|
||||
|
||||
sendExtensionRuntimeMessage({ command: "offscreenCopyToClipboard", text });
|
||||
await flushPromises();
|
||||
|
||||
expect(browserClipboardServiceCopySpy).toHaveBeenCalledWith(window, text);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleOffscreenReadFromClipboard", () => {
|
||||
it("reads the value from the clipboard service", async () => {
|
||||
sendExtensionRuntimeMessage({ command: "offscreenReadFromClipboard" });
|
||||
await flushPromises();
|
||||
|
||||
expect(browserClipboardServiceReadSpy).toHaveBeenCalledWith(window);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,83 @@
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
import BrowserClipboardService from "../services/browser-clipboard.service";
|
||||
|
||||
import {
|
||||
OffscreenDocumentExtensionMessage,
|
||||
OffscreenDocumentExtensionMessageHandlers,
|
||||
OffscreenDocument as OffscreenDocumentInterface,
|
||||
} from "./abstractions/offscreen-document";
|
||||
|
||||
class OffscreenDocument implements OffscreenDocumentInterface {
|
||||
private consoleLogService: ConsoleLogService = new ConsoleLogService(false);
|
||||
private readonly extensionMessageHandlers: OffscreenDocumentExtensionMessageHandlers = {
|
||||
offscreenCopyToClipboard: ({ message }) => this.handleOffscreenCopyToClipboard(message),
|
||||
offscreenReadFromClipboard: () => this.handleOffscreenReadFromClipboard(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the offscreen document extension.
|
||||
*/
|
||||
init() {
|
||||
this.setupExtensionMessageListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the given text to the user's clipboard.
|
||||
*
|
||||
* @param message - The extension message containing the text to copy
|
||||
*/
|
||||
private async handleOffscreenCopyToClipboard(message: OffscreenDocumentExtensionMessage) {
|
||||
await BrowserClipboardService.copy(window, message.text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the user's clipboard and returns the text.
|
||||
*/
|
||||
private async handleOffscreenReadFromClipboard() {
|
||||
return await BrowserClipboardService.read(window);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the listener for extension messages.
|
||||
*/
|
||||
private setupExtensionMessageListener() {
|
||||
BrowserApi.messageListener("offscreen-document", this.handleExtensionMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles extension messages sent to the extension background.
|
||||
*
|
||||
* @param message - The message received from the extension
|
||||
* @param sender - The sender of the message
|
||||
* @param sendResponse - The response to send back to the sender
|
||||
*/
|
||||
private handleExtensionMessage = (
|
||||
message: OffscreenDocumentExtensionMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
sendResponse: (response?: any) => void,
|
||||
) => {
|
||||
const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command];
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageResponse = handler({ message, sender });
|
||||
if (!messageResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.resolve(messageResponse)
|
||||
.then((response) => sendResponse(response))
|
||||
.catch((error) =>
|
||||
this.consoleLogService.error(`Error resolving extension message response: ${error}`),
|
||||
);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
(() => {
|
||||
const offscreenDocument = new OffscreenDocument();
|
||||
offscreenDocument.init();
|
||||
})();
|
@ -93,7 +93,7 @@ class BrowserPopupUtils {
|
||||
* Identifies if the popup is loading in private mode.
|
||||
*/
|
||||
static inPrivateMode() {
|
||||
return BrowserPopupUtils.backgroundInitializationRequired() && BrowserApi.manifestVersion !== 3;
|
||||
return BrowserPopupUtils.backgroundInitializationRequired() && !BrowserApi.isManifestVersion(3);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,111 @@
|
||||
import BrowserClipboardService from "./browser-clipboard.service";
|
||||
|
||||
describe("BrowserClipboardService", () => {
|
||||
let windowMock: any;
|
||||
const consoleWarnSpy = jest.spyOn(console, "warn");
|
||||
|
||||
beforeEach(() => {
|
||||
windowMock = {
|
||||
navigator: {
|
||||
clipboard: {
|
||||
writeText: jest.fn(),
|
||||
readText: jest.fn(),
|
||||
},
|
||||
},
|
||||
document: {
|
||||
body: {
|
||||
appendChild: jest.fn((element) => document.body.appendChild(element)),
|
||||
removeChild: jest.fn((element) => document.body.removeChild(element)),
|
||||
},
|
||||
createElement: jest.fn((tagName) => document.createElement(tagName)),
|
||||
execCommand: jest.fn(),
|
||||
queryCommandSupported: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("copy", () => {
|
||||
it("uses the legacy copy method if the clipboard API is not available", async () => {
|
||||
const text = "test";
|
||||
windowMock.navigator.clipboard = {};
|
||||
windowMock.document.queryCommandSupported.mockReturnValue(true);
|
||||
|
||||
await BrowserClipboardService.copy(windowMock as Window, text);
|
||||
|
||||
expect(windowMock.document.execCommand).toHaveBeenCalledWith("copy");
|
||||
});
|
||||
|
||||
it("uses the legacy copy method if the clipboard API throws an error", async () => {
|
||||
windowMock.document.queryCommandSupported.mockReturnValue(true);
|
||||
windowMock.navigator.clipboard.writeText.mockRejectedValue(new Error("test"));
|
||||
|
||||
await BrowserClipboardService.copy(windowMock as Window, "test");
|
||||
|
||||
expect(windowMock.document.execCommand).toHaveBeenCalledWith("copy");
|
||||
});
|
||||
|
||||
it("copies the given text to the clipboard", async () => {
|
||||
const text = "test";
|
||||
|
||||
await BrowserClipboardService.copy(windowMock as Window, text);
|
||||
|
||||
expect(windowMock.navigator.clipboard.writeText).toHaveBeenCalledWith(text);
|
||||
});
|
||||
|
||||
it("prints an warning message to the console if both the clipboard api and legacy method throw an error", async () => {
|
||||
windowMock.document.queryCommandSupported.mockReturnValue(true);
|
||||
windowMock.navigator.clipboard.writeText.mockRejectedValue(new Error("test"));
|
||||
windowMock.document.execCommand.mockImplementation(() => {
|
||||
throw new Error("test");
|
||||
});
|
||||
|
||||
await BrowserClipboardService.copy(windowMock as Window, "");
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("read", () => {
|
||||
it("uses the legacy read method if the clipboard API is not available", async () => {
|
||||
const testValue = "test";
|
||||
windowMock.navigator.clipboard = {};
|
||||
windowMock.document.queryCommandSupported.mockReturnValue(true);
|
||||
windowMock.document.execCommand.mockImplementation(() => {
|
||||
document.querySelector("textarea").value = testValue;
|
||||
return true;
|
||||
});
|
||||
|
||||
const returnValue = await BrowserClipboardService.read(windowMock as Window);
|
||||
|
||||
expect(windowMock.document.execCommand).toHaveBeenCalledWith("paste");
|
||||
expect(returnValue).toBe(testValue);
|
||||
});
|
||||
|
||||
it("uses the legacy read method if the clipboard API throws an error", async () => {
|
||||
windowMock.document.queryCommandSupported.mockReturnValue(true);
|
||||
windowMock.navigator.clipboard.readText.mockRejectedValue(new Error("test"));
|
||||
|
||||
await BrowserClipboardService.read(windowMock as Window);
|
||||
|
||||
expect(windowMock.document.execCommand).toHaveBeenCalledWith("paste");
|
||||
});
|
||||
|
||||
it("reads the text from the clipboard", async () => {
|
||||
await BrowserClipboardService.read(windowMock as Window);
|
||||
|
||||
expect(windowMock.navigator.clipboard.readText).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prints a warning message to the console if both the clipboard api and legacy method throw an error", async () => {
|
||||
windowMock.document.queryCommandSupported.mockReturnValue(true);
|
||||
windowMock.navigator.clipboard.readText.mockRejectedValue(new Error("test"));
|
||||
windowMock.document.execCommand.mockImplementation(() => {
|
||||
throw new Error("test");
|
||||
});
|
||||
|
||||
await BrowserClipboardService.read(windowMock as Window);
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
130
apps/browser/src/platform/services/browser-clipboard.service.ts
Normal file
130
apps/browser/src/platform/services/browser-clipboard.service.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
|
||||
class BrowserClipboardService {
|
||||
private static consoleLogService: ConsoleLogService = new ConsoleLogService(false);
|
||||
|
||||
/**
|
||||
* Copies the given text to the user's clipboard.
|
||||
*
|
||||
* @param globalContext - The global window context.
|
||||
* @param text - The text to copy.
|
||||
*/
|
||||
static async copy(globalContext: Window, text: string) {
|
||||
if (!BrowserClipboardService.isClipboardApiSupported(globalContext, "writeText")) {
|
||||
this.useLegacyCopyMethod(globalContext, text);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await globalContext.navigator.clipboard.writeText(text);
|
||||
} catch (error) {
|
||||
BrowserClipboardService.consoleLogService.debug(
|
||||
`Error copying to clipboard using the clipboard API, attempting legacy method: ${error}`,
|
||||
);
|
||||
|
||||
this.useLegacyCopyMethod(globalContext, text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the user's clipboard and returns the text.
|
||||
*
|
||||
* @param globalContext - The global window context.
|
||||
*/
|
||||
static async read(globalContext: Window): Promise<string> {
|
||||
if (!BrowserClipboardService.isClipboardApiSupported(globalContext, "readText")) {
|
||||
return this.useLegacyReadMethod(globalContext);
|
||||
}
|
||||
|
||||
try {
|
||||
return await globalContext.navigator.clipboard.readText();
|
||||
} catch (error) {
|
||||
BrowserClipboardService.consoleLogService.debug(
|
||||
`Error reading from clipboard using the clipboard API, attempting legacy method: ${error}`,
|
||||
);
|
||||
|
||||
return this.useLegacyReadMethod(globalContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the given text to the user's clipboard using the legacy `execCommand` method. This
|
||||
* method is used as a fallback when the clipboard API is not supported or fails.
|
||||
*
|
||||
* @param globalContext - The global window context.
|
||||
* @param text - The text to copy.
|
||||
*/
|
||||
private static useLegacyCopyMethod(globalContext: Window, text: string) {
|
||||
if (!BrowserClipboardService.isLegacyClipboardMethodSupported(globalContext, "copy")) {
|
||||
BrowserClipboardService.consoleLogService.warning("Legacy copy method not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
const textareaElement = globalContext.document.createElement("textarea");
|
||||
textareaElement.textContent = !text ? " " : text;
|
||||
textareaElement.style.position = "fixed";
|
||||
globalContext.document.body.appendChild(textareaElement);
|
||||
textareaElement.select();
|
||||
|
||||
try {
|
||||
globalContext.document.execCommand("copy");
|
||||
} catch (error) {
|
||||
BrowserClipboardService.consoleLogService.warning(`Error writing to clipboard: ${error}`);
|
||||
} finally {
|
||||
globalContext.document.body.removeChild(textareaElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the user's clipboard using the legacy `execCommand` method. This method is used as a
|
||||
* fallback when the clipboard API is not supported or fails.
|
||||
*
|
||||
* @param globalContext - The global window context.
|
||||
*/
|
||||
private static useLegacyReadMethod(globalContext: Window): string {
|
||||
if (!BrowserClipboardService.isLegacyClipboardMethodSupported(globalContext, "paste")) {
|
||||
BrowserClipboardService.consoleLogService.warning("Legacy paste method not supported");
|
||||
return "";
|
||||
}
|
||||
|
||||
const textareaElement = globalContext.document.createElement("textarea");
|
||||
textareaElement.style.position = "fixed";
|
||||
globalContext.document.body.appendChild(textareaElement);
|
||||
textareaElement.focus();
|
||||
|
||||
try {
|
||||
return globalContext.document.execCommand("paste") ? textareaElement.value : "";
|
||||
} catch (error) {
|
||||
BrowserClipboardService.consoleLogService.warning(`Error reading from clipboard: ${error}`);
|
||||
} finally {
|
||||
globalContext.document.body.removeChild(textareaElement);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the clipboard API is supported in the current environment.
|
||||
*
|
||||
* @param globalContext - The global window context.
|
||||
* @param method - The clipboard API method to check for support.
|
||||
*/
|
||||
private static isClipboardApiSupported(globalContext: Window, method: "writeText" | "readText") {
|
||||
return "clipboard" in globalContext.navigator && method in globalContext.navigator.clipboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the legacy clipboard method is supported in the current environment.
|
||||
*
|
||||
* @param globalContext - The global window context.
|
||||
* @param method - The legacy clipboard method to check for support.
|
||||
*/
|
||||
private static isLegacyClipboardMethodSupported(globalContext: Window, method: "copy" | "paste") {
|
||||
return (
|
||||
"queryCommandSupported" in globalContext.document &&
|
||||
globalContext.document.queryCommandSupported(method)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BrowserClipboardService;
|
@ -1,14 +1,24 @@
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
|
||||
import { flushPromises } from "../../autofill/spec/testing-utils";
|
||||
import { SafariApp } from "../../browser/safariApp";
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
|
||||
import BrowserClipboardService from "./browser-clipboard.service";
|
||||
import BrowserPlatformUtilsService from "./browser-platform-utils.service";
|
||||
|
||||
describe("Browser Utils Service", () => {
|
||||
let browserPlatformUtilsService: BrowserPlatformUtilsService;
|
||||
const clipboardWriteCallbackSpy = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
(window as any).matchMedia = jest.fn().mockReturnValueOnce({});
|
||||
browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null, window);
|
||||
browserPlatformUtilsService = new BrowserPlatformUtilsService(
|
||||
null,
|
||||
clipboardWriteCallbackSpy,
|
||||
null,
|
||||
window,
|
||||
);
|
||||
});
|
||||
|
||||
describe("getBrowser", () => {
|
||||
@ -26,7 +36,6 @@ describe("Browser Utils Service", () => {
|
||||
|
||||
afterEach(() => {
|
||||
window.matchMedia = undefined;
|
||||
(window as any).chrome = undefined;
|
||||
(BrowserPlatformUtilsService as any).deviceCache = null;
|
||||
});
|
||||
|
||||
@ -37,8 +46,6 @@ describe("Browser Utils Service", () => {
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
|
||||
});
|
||||
|
||||
(window as any).chrome = {};
|
||||
|
||||
expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.ChromeExtension);
|
||||
});
|
||||
|
||||
@ -90,6 +97,29 @@ describe("Browser Utils Service", () => {
|
||||
|
||||
expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.VivaldiExtension);
|
||||
});
|
||||
|
||||
it("returns a previously determined device using a cached value", () => {
|
||||
Object.defineProperty(navigator, "userAgent", {
|
||||
configurable: true,
|
||||
value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0",
|
||||
});
|
||||
jest.spyOn(BrowserPlatformUtilsService, "isFirefox");
|
||||
|
||||
browserPlatformUtilsService.getDevice();
|
||||
|
||||
expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.FirefoxExtension);
|
||||
expect(BrowserPlatformUtilsService.isFirefox).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDeviceString", () => {
|
||||
it("returns a string value indicating the device type", () => {
|
||||
jest
|
||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.ChromeExtension);
|
||||
|
||||
expect(browserPlatformUtilsService.getDeviceString()).toBe("chrome");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isViewOpen", () => {
|
||||
@ -113,6 +143,176 @@ describe("Browser Utils Service", () => {
|
||||
expect(isViewOpen).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("copyToClipboard", () => {
|
||||
const getManifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
|
||||
const sendMessageToAppSpy = jest.spyOn(SafariApp, "sendMessageToApp");
|
||||
const clipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy");
|
||||
let triggerOffscreenCopyToClipboardSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
getManifestVersionSpy.mockReturnValue(2);
|
||||
triggerOffscreenCopyToClipboardSpy = jest.spyOn(
|
||||
browserPlatformUtilsService as any,
|
||||
"triggerOffscreenCopyToClipboard",
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("sends a copy to clipboard message to the desktop application if a user is using the safari browser", async () => {
|
||||
const text = "test";
|
||||
const clearMs = 1000;
|
||||
sendMessageToAppSpy.mockResolvedValueOnce("success");
|
||||
jest
|
||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.SafariExtension);
|
||||
|
||||
browserPlatformUtilsService.copyToClipboard(text, { clearMs });
|
||||
await flushPromises();
|
||||
|
||||
expect(sendMessageToAppSpy).toHaveBeenCalledWith("copyToClipboard", text);
|
||||
expect(clipboardWriteCallbackSpy).toHaveBeenCalledWith(text, clearMs);
|
||||
expect(clipboardServiceCopySpy).not.toHaveBeenCalled();
|
||||
expect(triggerOffscreenCopyToClipboardSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets the copied text to a unicode placeholder when the user is using Chrome if the passed text is an empty string", async () => {
|
||||
const text = "";
|
||||
jest
|
||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.ChromeExtension);
|
||||
|
||||
browserPlatformUtilsService.copyToClipboard(text);
|
||||
await flushPromises();
|
||||
|
||||
expect(clipboardServiceCopySpy).toHaveBeenCalledWith(window, "\u0000");
|
||||
});
|
||||
|
||||
it("copies the passed text using the BrowserClipboardService", async () => {
|
||||
const text = "test";
|
||||
jest
|
||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.ChromeExtension);
|
||||
|
||||
browserPlatformUtilsService.copyToClipboard(text, { window: self });
|
||||
await flushPromises();
|
||||
|
||||
expect(clipboardServiceCopySpy).toHaveBeenCalledWith(self, text);
|
||||
expect(triggerOffscreenCopyToClipboardSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("copies the passed text using the offscreen document if the extension is using manifest v3", async () => {
|
||||
const text = "test";
|
||||
jest
|
||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.ChromeExtension);
|
||||
getManifestVersionSpy.mockReturnValue(3);
|
||||
jest.spyOn(BrowserApi, "createOffscreenDocument");
|
||||
jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue(undefined);
|
||||
jest.spyOn(BrowserApi, "closeOffscreenDocument");
|
||||
|
||||
browserPlatformUtilsService.copyToClipboard(text);
|
||||
await flushPromises();
|
||||
|
||||
expect(triggerOffscreenCopyToClipboardSpy).toHaveBeenCalledWith(text);
|
||||
expect(clipboardServiceCopySpy).not.toHaveBeenCalled();
|
||||
expect(BrowserApi.createOffscreenDocument).toHaveBeenCalledWith(
|
||||
[chrome.offscreen.Reason.CLIPBOARD],
|
||||
"Write text to the clipboard.",
|
||||
);
|
||||
expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenCopyToClipboard", {
|
||||
text,
|
||||
});
|
||||
expect(BrowserApi.closeOffscreenDocument).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips the clipboardWriteCallback if the clipboard is clearing", async () => {
|
||||
jest
|
||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.ChromeExtension);
|
||||
|
||||
browserPlatformUtilsService.copyToClipboard("test", { window: self, clearing: true });
|
||||
await flushPromises();
|
||||
|
||||
expect(clipboardWriteCallbackSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("readFromClipboard", () => {
|
||||
const getManifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
|
||||
const sendMessageToAppSpy = jest.spyOn(SafariApp, "sendMessageToApp");
|
||||
const clipboardServiceReadSpy = jest.spyOn(BrowserClipboardService, "read");
|
||||
|
||||
beforeEach(() => {
|
||||
getManifestVersionSpy.mockReturnValue(2);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("sends a ready from clipboard message to the desktop application if a user is using the safari browser", async () => {
|
||||
sendMessageToAppSpy.mockResolvedValueOnce("test");
|
||||
jest
|
||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.SafariExtension);
|
||||
|
||||
const result = await browserPlatformUtilsService.readFromClipboard();
|
||||
|
||||
expect(sendMessageToAppSpy).toHaveBeenCalledWith("readFromClipboard");
|
||||
expect(clipboardServiceReadSpy).not.toHaveBeenCalled();
|
||||
expect(result).toBe("test");
|
||||
});
|
||||
|
||||
it("reads text from the clipboard using the ClipboardService", async () => {
|
||||
jest
|
||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.ChromeExtension);
|
||||
clipboardServiceReadSpy.mockResolvedValueOnce("test");
|
||||
|
||||
const result = await browserPlatformUtilsService.readFromClipboard({ window: self });
|
||||
|
||||
expect(clipboardServiceReadSpy).toHaveBeenCalledWith(self);
|
||||
expect(sendMessageToAppSpy).not.toHaveBeenCalled();
|
||||
expect(result).toBe("test");
|
||||
});
|
||||
|
||||
it("reads the clipboard text using the offscreen document", async () => {
|
||||
jest
|
||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.ChromeExtension);
|
||||
getManifestVersionSpy.mockReturnValue(3);
|
||||
jest.spyOn(BrowserApi, "createOffscreenDocument");
|
||||
jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue("test");
|
||||
jest.spyOn(BrowserApi, "closeOffscreenDocument");
|
||||
|
||||
await browserPlatformUtilsService.readFromClipboard();
|
||||
|
||||
expect(BrowserApi.createOffscreenDocument).toHaveBeenCalledWith(
|
||||
[chrome.offscreen.Reason.CLIPBOARD],
|
||||
"Read text from the clipboard.",
|
||||
);
|
||||
expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenReadFromClipboard");
|
||||
expect(BrowserApi.closeOffscreenDocument).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns an empty string from the offscreen document if the response is not of type string", async () => {
|
||||
jest
|
||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.ChromeExtension);
|
||||
getManifestVersionSpy.mockReturnValue(3);
|
||||
jest.spyOn(BrowserApi, "createOffscreenDocument");
|
||||
jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue(1);
|
||||
jest.spyOn(BrowserApi, "closeOffscreenDocument");
|
||||
|
||||
const result = await browserPlatformUtilsService.readFromClipboard();
|
||||
|
||||
expect(result).toBe("");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Safari Height Fix", () => {
|
||||
|
@ -1,10 +1,15 @@
|
||||
import { ClientType, DeviceType } from "@bitwarden/common/enums";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
ClipboardOptions,
|
||||
PlatformUtilsService,
|
||||
} from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { SafariApp } from "../../browser/safariApp";
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
|
||||
import BrowserClipboardService from "./browser-clipboard.service";
|
||||
|
||||
export default class BrowserPlatformUtilsService implements PlatformUtilsService {
|
||||
private static deviceCache: DeviceType = null;
|
||||
|
||||
@ -12,25 +17,25 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
|
||||
private messagingService: MessagingService,
|
||||
private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
|
||||
private biometricCallback: () => Promise<boolean>,
|
||||
private win: Window & typeof globalThis,
|
||||
private globalContext: Window | ServiceWorkerGlobalScope,
|
||||
) {}
|
||||
|
||||
static getDevice(win: Window & typeof globalThis): DeviceType {
|
||||
static getDevice(globalContext: Window | ServiceWorkerGlobalScope): DeviceType {
|
||||
if (this.deviceCache) {
|
||||
return this.deviceCache;
|
||||
}
|
||||
|
||||
if (BrowserPlatformUtilsService.isFirefox()) {
|
||||
this.deviceCache = DeviceType.FirefoxExtension;
|
||||
} else if (BrowserPlatformUtilsService.isOpera(win)) {
|
||||
} else if (BrowserPlatformUtilsService.isOpera(globalContext)) {
|
||||
this.deviceCache = DeviceType.OperaExtension;
|
||||
} else if (BrowserPlatformUtilsService.isEdge()) {
|
||||
this.deviceCache = DeviceType.EdgeExtension;
|
||||
} else if (BrowserPlatformUtilsService.isVivaldi()) {
|
||||
this.deviceCache = DeviceType.VivaldiExtension;
|
||||
} else if (BrowserPlatformUtilsService.isChrome(win)) {
|
||||
} else if (BrowserPlatformUtilsService.isChrome(globalContext)) {
|
||||
this.deviceCache = DeviceType.ChromeExtension;
|
||||
} else if (BrowserPlatformUtilsService.isSafari(win)) {
|
||||
} else if (BrowserPlatformUtilsService.isSafari(globalContext)) {
|
||||
this.deviceCache = DeviceType.SafariExtension;
|
||||
}
|
||||
|
||||
@ -38,7 +43,7 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
|
||||
}
|
||||
|
||||
getDevice(): DeviceType {
|
||||
return BrowserPlatformUtilsService.getDevice(this.win);
|
||||
return BrowserPlatformUtilsService.getDevice(this.globalContext);
|
||||
}
|
||||
|
||||
getDeviceString(): string {
|
||||
@ -67,8 +72,8 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
|
||||
/**
|
||||
* @deprecated Do not call this directly, use getDevice() instead
|
||||
*/
|
||||
private static isChrome(win: Window & typeof globalThis): boolean {
|
||||
return win.chrome && navigator.userAgent.indexOf(" Chrome/") !== -1;
|
||||
private static isChrome(globalContext: Window | ServiceWorkerGlobalScope): boolean {
|
||||
return globalContext.chrome && navigator.userAgent.indexOf(" Chrome/") !== -1;
|
||||
}
|
||||
|
||||
isChrome(): boolean {
|
||||
@ -89,9 +94,11 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
|
||||
/**
|
||||
* @deprecated Do not call this directly, use getDevice() instead
|
||||
*/
|
||||
private static isOpera(win: Window & typeof globalThis): boolean {
|
||||
private static isOpera(globalContext: Window | ServiceWorkerGlobalScope): boolean {
|
||||
return (
|
||||
(!!win.opr && !!win.opr.addons) || !!win.opera || navigator.userAgent.indexOf(" OPR/") >= 0
|
||||
!!globalContext.opr?.addons ||
|
||||
!!globalContext.opera ||
|
||||
navigator.userAgent.indexOf(" OPR/") >= 0
|
||||
);
|
||||
}
|
||||
|
||||
@ -113,10 +120,11 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
|
||||
/**
|
||||
* @deprecated Do not call this directly, use getDevice() instead
|
||||
*/
|
||||
static isSafari(win: Window & typeof globalThis): boolean {
|
||||
static isSafari(globalContext: Window | ServiceWorkerGlobalScope): boolean {
|
||||
// Opera masquerades as Safari, so make sure we're not there first
|
||||
return (
|
||||
!BrowserPlatformUtilsService.isOpera(win) && navigator.userAgent.indexOf(" Safari/") !== -1
|
||||
!BrowserPlatformUtilsService.isOpera(globalContext) &&
|
||||
navigator.userAgent.indexOf(" Safari/") !== -1
|
||||
);
|
||||
}
|
||||
|
||||
@ -128,8 +136,8 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
|
||||
* Safari previous to version 16.1 had a bug which caused artifacts on hover in large extension popups.
|
||||
* https://bugs.webkit.org/show_bug.cgi?id=218704
|
||||
*/
|
||||
static shouldApplySafariHeightFix(win: Window & typeof globalThis): boolean {
|
||||
if (BrowserPlatformUtilsService.getDevice(win) !== DeviceType.SafariExtension) {
|
||||
static shouldApplySafariHeightFix(globalContext: Window | ServiceWorkerGlobalScope): boolean {
|
||||
if (BrowserPlatformUtilsService.getDevice(globalContext) !== DeviceType.SafariExtension) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -207,99 +215,66 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
|
||||
return false;
|
||||
}
|
||||
|
||||
copyToClipboard(text: string, options?: any): void {
|
||||
let win = this.win;
|
||||
let doc = this.win.document;
|
||||
if (options && (options.window || options.win)) {
|
||||
win = options.window || options.win;
|
||||
doc = win.document;
|
||||
} else if (options && options.doc) {
|
||||
doc = options.doc;
|
||||
/**
|
||||
* Copies the passed text to the clipboard. For Safari, this will use
|
||||
* the native messaging API to send the text to the Bitwarden app. If
|
||||
* the extension is using manifest v3, the offscreen document API will
|
||||
* be used to copy the text to the clipboard. Otherwise, the browser's
|
||||
* clipboard API will be used.
|
||||
*
|
||||
* @param text - The text to copy to the clipboard.
|
||||
* @param options - Options for the clipboard operation.
|
||||
*/
|
||||
copyToClipboard(text: string, options?: ClipboardOptions): void {
|
||||
const windowContext = options?.window || (this.globalContext as Window);
|
||||
const clearing = Boolean(options?.clearing);
|
||||
const clearMs: number = options?.clearMs || null;
|
||||
const handleClipboardWriteCallback = () => {
|
||||
if (!clearing && this.clipboardWriteCallback != null) {
|
||||
this.clipboardWriteCallback(text, clearMs);
|
||||
}
|
||||
const clearing = options ? !!options.clearing : false;
|
||||
const clearMs: number = options && options.clearMs ? options.clearMs : null;
|
||||
};
|
||||
|
||||
if (this.isSafari()) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
SafariApp.sendMessageToApp("copyToClipboard", text).then(() => {
|
||||
if (!clearing && this.clipboardWriteCallback != null) {
|
||||
this.clipboardWriteCallback(text, clearMs);
|
||||
void SafariApp.sendMessageToApp("copyToClipboard", text).then(handleClipboardWriteCallback);
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
} else if (
|
||||
this.isFirefox() &&
|
||||
(win as any).navigator.clipboard &&
|
||||
(win as any).navigator.clipboard.writeText
|
||||
) {
|
||||
(win as any).navigator.clipboard.writeText(text).then(() => {
|
||||
if (!clearing && this.clipboardWriteCallback != null) {
|
||||
this.clipboardWriteCallback(text, clearMs);
|
||||
}
|
||||
});
|
||||
} else if (doc.queryCommandSupported && doc.queryCommandSupported("copy")) {
|
||||
|
||||
if (this.isChrome() && text === "") {
|
||||
text = "\u0000";
|
||||
}
|
||||
|
||||
const textarea = doc.createElement("textarea");
|
||||
textarea.textContent = text == null || text === "" ? " " : text;
|
||||
// Prevent scrolling to bottom of page in MS Edge.
|
||||
textarea.style.position = "fixed";
|
||||
doc.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
if (this.isChrome() && BrowserApi.isManifestVersion(3)) {
|
||||
void this.triggerOffscreenCopyToClipboard(text).then(handleClipboardWriteCallback);
|
||||
|
||||
try {
|
||||
// Security exception may be thrown by some browsers.
|
||||
if (doc.execCommand("copy") && !clearing && this.clipboardWriteCallback != null) {
|
||||
this.clipboardWriteCallback(text, clearMs);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line
|
||||
console.warn("Copy to clipboard failed.", e);
|
||||
} finally {
|
||||
doc.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async readFromClipboard(options?: any): Promise<string> {
|
||||
let win = this.win;
|
||||
let doc = this.win.document;
|
||||
if (options && (options.window || options.win)) {
|
||||
win = options.window || options.win;
|
||||
doc = win.document;
|
||||
} else if (options && options.doc) {
|
||||
doc = options.doc;
|
||||
void BrowserClipboardService.copy(windowContext, text).then(handleClipboardWriteCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the text from the clipboard. For Safari, this will use the
|
||||
* native messaging API to request the text from the Bitwarden app. If
|
||||
* the extension is using manifest v3, the offscreen document API will
|
||||
* be used to read the text from the clipboard. Otherwise, the browser's
|
||||
* clipboard API will be used.
|
||||
*
|
||||
* @param options - Options for the clipboard operation.
|
||||
*/
|
||||
async readFromClipboard(options?: ClipboardOptions): Promise<string> {
|
||||
const windowContext = options?.window || (this.globalContext as Window);
|
||||
|
||||
if (this.isSafari()) {
|
||||
return await SafariApp.sendMessageToApp("readFromClipboard");
|
||||
} else if (
|
||||
this.isFirefox() &&
|
||||
(win as any).navigator.clipboard &&
|
||||
(win as any).navigator.clipboard.readText
|
||||
) {
|
||||
return await (win as any).navigator.clipboard.readText();
|
||||
} else if (doc.queryCommandSupported && doc.queryCommandSupported("paste")) {
|
||||
const textarea = doc.createElement("textarea");
|
||||
// Prevent scrolling to bottom of page in MS Edge.
|
||||
textarea.style.position = "fixed";
|
||||
doc.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
try {
|
||||
// Security exception may be thrown by some browsers.
|
||||
if (doc.execCommand("paste")) {
|
||||
return textarea.value;
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line
|
||||
console.warn("Read from clipboard failed.", e);
|
||||
} finally {
|
||||
doc.body.removeChild(textarea);
|
||||
|
||||
if (this.isChrome() && BrowserApi.isManifestVersion(3)) {
|
||||
return await this.triggerOffscreenReadFromClipboard();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
return await BrowserClipboardService.read(windowContext);
|
||||
}
|
||||
|
||||
async supportsBiometric() {
|
||||
@ -345,4 +320,33 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
|
||||
}
|
||||
return autofillCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the offscreen document API to copy the text to the clipboard.
|
||||
*/
|
||||
private async triggerOffscreenCopyToClipboard(text: string) {
|
||||
await BrowserApi.createOffscreenDocument(
|
||||
[chrome.offscreen.Reason.CLIPBOARD],
|
||||
"Write text to the clipboard.",
|
||||
);
|
||||
await BrowserApi.sendMessageWithResponse("offscreenCopyToClipboard", { text });
|
||||
BrowserApi.closeOffscreenDocument();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the offscreen document API to read the text from the clipboard.
|
||||
*/
|
||||
private async triggerOffscreenReadFromClipboard() {
|
||||
await BrowserApi.createOffscreenDocument(
|
||||
[chrome.offscreen.Reason.CLIPBOARD],
|
||||
"Read text from the clipboard.",
|
||||
);
|
||||
const response = await BrowserApi.sendMessageWithResponse("offscreenReadFromClipboard");
|
||||
BrowserApi.closeOffscreenDocument();
|
||||
if (typeof response === "string") {
|
||||
return response;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
|
||||
|
||||
(function (globalContext) {
|
||||
globalContext.postMessage({ command: VaultOnboardingMessages.HasBwInstalled });
|
||||
})(window);
|
@ -67,6 +67,7 @@ export type BrowserFido2Message = { sessionId: string } & (
|
||||
userName: string;
|
||||
userVerification: boolean;
|
||||
fallbackSupported: boolean;
|
||||
rpId: string;
|
||||
}
|
||||
| {
|
||||
type: "ConfirmNewCredentialResponse";
|
||||
@ -242,6 +243,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
credentialName,
|
||||
userName,
|
||||
userVerification,
|
||||
rpId,
|
||||
}: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
|
||||
const data: BrowserFido2Message = {
|
||||
type: "ConfirmNewCredentialRequest",
|
||||
@ -250,6 +252,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
userName,
|
||||
userVerification,
|
||||
fallbackSupported: this.fallbackSupported,
|
||||
rpId,
|
||||
};
|
||||
|
||||
await this.send(data);
|
||||
|
@ -16,7 +16,6 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SecureNoteType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
@ -245,7 +244,8 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
protected async saveNewLogin() {
|
||||
const data = this.message$.value;
|
||||
if (data?.type === "ConfirmNewCredentialRequest") {
|
||||
await this.createNewCipher();
|
||||
const name = data.credentialName || data.rpId;
|
||||
await this.createNewCipher(name);
|
||||
|
||||
// We are bypassing user verification pending implementation of PIN and biometric support.
|
||||
this.send({
|
||||
@ -296,7 +296,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/add-cipher"], {
|
||||
queryParams: {
|
||||
name: Utils.getHostname(this.url),
|
||||
name: data.credentialName || data.rpId,
|
||||
uri: this.url,
|
||||
uilocation: "popout",
|
||||
senderTabId: this.senderTabId,
|
||||
@ -344,9 +344,9 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private buildCipher() {
|
||||
private buildCipher(name: string) {
|
||||
this.cipher = new CipherView();
|
||||
this.cipher.name = Utils.getHostname(this.url);
|
||||
this.cipher.name = name;
|
||||
this.cipher.type = CipherType.Login;
|
||||
this.cipher.login = new LoginView();
|
||||
this.cipher.login.uris = [new LoginUriView()];
|
||||
@ -358,8 +358,8 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
this.cipher.reprompt = CipherRepromptType.None;
|
||||
}
|
||||
|
||||
private async createNewCipher() {
|
||||
this.buildCipher();
|
||||
private async createNewCipher(name: string) {
|
||||
this.buildCipher(name);
|
||||
const cipher = await this.cipherService.encrypt(this.cipher);
|
||||
try {
|
||||
await this.cipherService.createWithServer(cipher);
|
||||
|
@ -44,6 +44,7 @@ const i18n = {
|
||||
};
|
||||
|
||||
const tabs = {
|
||||
get: jest.fn(),
|
||||
executeScript: jest.fn(),
|
||||
sendMessage: jest.fn(),
|
||||
query: jest.fn(),
|
||||
@ -111,6 +112,18 @@ const extension = {
|
||||
getViews: jest.fn(),
|
||||
};
|
||||
|
||||
const offscreen = {
|
||||
createDocument: jest.fn(),
|
||||
closeDocument: jest.fn((callback) => {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}),
|
||||
Reason: {
|
||||
CLIPBOARD: "clipboard",
|
||||
},
|
||||
};
|
||||
|
||||
// set chrome
|
||||
global.chrome = {
|
||||
i18n,
|
||||
@ -123,4 +136,5 @@ global.chrome = {
|
||||
port,
|
||||
privacy,
|
||||
extension,
|
||||
offscreen,
|
||||
} as any;
|
||||
|
@ -179,6 +179,7 @@ const mainConfig = {
|
||||
"overlay/list": "./src/autofill/overlay/pages/list/bootstrap-autofill-overlay-list.ts",
|
||||
"encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.ts",
|
||||
"content/lp-fileless-importer": "./src/tools/content/lp-fileless-importer.ts",
|
||||
"content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts",
|
||||
"content/lp-suppress-import-download": "./src/tools/content/lp-suppress-import-download.ts",
|
||||
},
|
||||
optimization: {
|
||||
@ -285,6 +286,16 @@ if (manifestVersion == 2) {
|
||||
// Manifest v3 needs an extra helper for utilities in the content script.
|
||||
// The javascript output of this should be added to manifest.v3.json
|
||||
mainConfig.entry["content/misc-utils"] = "./src/autofill/content/misc-utils.ts";
|
||||
mainConfig.entry["offscreen-document/offscreen-document"] =
|
||||
"./src/platform/offscreen-document/offscreen-document.ts";
|
||||
|
||||
mainConfig.plugins.push(
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/platform/offscreen-document/index.html",
|
||||
filename: "offscreen-document/index.html",
|
||||
chunks: ["offscreen-document/offscreen-document"],
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @type {import("webpack").Configuration}
|
||||
|
4
apps/desktop/desktop_native/Cargo.lock
generated
4
apps/desktop/desktop_native/Cargo.lock
generated
@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
|
@ -13,7 +13,7 @@ default = []
|
||||
manual_test = []
|
||||
|
||||
[dependencies]
|
||||
aes = "=0.8.3"
|
||||
aes = "=0.8.4"
|
||||
anyhow = "=1.0.80"
|
||||
arboard = { version = "=3.3.0", default-features = false, features = ["wayland-data-control"] }
|
||||
base64 = "=0.21.5"
|
||||
|
@ -167,6 +167,7 @@
|
||||
},
|
||||
"snap": {
|
||||
"autoStart": true,
|
||||
"base": "core22",
|
||||
"confinement": "strict",
|
||||
"plugs": ["default", "password-manager-service"],
|
||||
"stagePackages": ["default"]
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2024.2.4",
|
||||
"version": "2024.2.5",
|
||||
"scripts": {
|
||||
"build:oss": "webpack",
|
||||
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||
|
@ -37,7 +37,7 @@ import {
|
||||
} from "../../../shared/components/access-selector";
|
||||
|
||||
import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator";
|
||||
import { orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator } from "./validators/org-without-additional-seat-limit-reached-with-upgrade-path.validator";
|
||||
import { orgSeatLimitReachedValidator } from "./validators/org-seat-limit-reached.validator";
|
||||
|
||||
export enum MemberDialogTab {
|
||||
Role = 0,
|
||||
@ -84,7 +84,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
protected groupAccessItems: AccessItemView[] = [];
|
||||
protected tabIndex: MemberDialogTab;
|
||||
protected formGroup = this.formBuilder.group({
|
||||
emails: ["", { updateOn: "blur" }],
|
||||
emails: [""],
|
||||
type: OrganizationUserType.User,
|
||||
externalId: this.formBuilder.control({ value: "", disabled: true }),
|
||||
accessAllCollections: false,
|
||||
@ -176,7 +176,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
const emailsControlValidators = [
|
||||
Validators.required,
|
||||
commaSeparatedEmails,
|
||||
orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
orgSeatLimitReachedValidator(
|
||||
this.organization,
|
||||
this.params.allOrganizationUserEmails,
|
||||
this.i18nService.t("subscriptionUpgrade", organization.seats),
|
||||
|
@ -4,7 +4,7 @@ import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
|
||||
import { orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator } from "./org-without-additional-seat-limit-reached-with-upgrade-path.validator";
|
||||
import { orgSeatLimitReachedValidator } from "./org-seat-limit-reached.validator";
|
||||
|
||||
const orgFactory = (props: Partial<Organization> = {}) =>
|
||||
Object.assign(
|
||||
@ -17,7 +17,7 @@ const orgFactory = (props: Partial<Organization> = {}) =>
|
||||
props,
|
||||
);
|
||||
|
||||
describe("orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator", () => {
|
||||
describe("orgSeatLimitReachedValidator", () => {
|
||||
let organization: Organization;
|
||||
let allOrganizationUserEmails: string[];
|
||||
let validatorFn: (control: AbstractControl) => ValidationErrors | null;
|
||||
@ -27,7 +27,7 @@ describe("orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator", () => {
|
||||
});
|
||||
|
||||
it("should return null when control value is empty", () => {
|
||||
validatorFn = orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
validatorFn = orgSeatLimitReachedValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 2 members without upgrading your plan.",
|
||||
@ -40,7 +40,7 @@ describe("orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator", () => {
|
||||
});
|
||||
|
||||
it("should return null when control value is null", () => {
|
||||
validatorFn = orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
validatorFn = orgSeatLimitReachedValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 2 members without upgrading your plan.",
|
||||
@ -57,7 +57,7 @@ describe("orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator", () => {
|
||||
planProductType: ProductType.Free,
|
||||
seats: 2,
|
||||
});
|
||||
validatorFn = orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
validatorFn = orgSeatLimitReachedValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 2 members without upgrading your plan.",
|
||||
@ -74,7 +74,7 @@ describe("orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator", () => {
|
||||
planProductType: ProductType.TeamsStarter,
|
||||
seats: 10,
|
||||
});
|
||||
validatorFn = orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
validatorFn = orgSeatLimitReachedValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 10 members without upgrading your plan.",
|
||||
@ -102,7 +102,7 @@ describe("orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator", () => {
|
||||
seats: 2,
|
||||
});
|
||||
const errorMessage = "You cannot invite more than 2 members without upgrading your plan.";
|
||||
validatorFn = orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
validatorFn = orgSeatLimitReachedValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 2 members without upgrading your plan.",
|
||||
@ -120,7 +120,7 @@ describe("orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator", () => {
|
||||
planProductType: ProductType.Enterprise,
|
||||
seats: 100,
|
||||
});
|
||||
validatorFn = orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
validatorFn = orgSeatLimitReachedValidator(
|
||||
organization,
|
||||
allOrganizationUserEmails,
|
||||
"You cannot invite more than 2 members without upgrading your plan.",
|
@ -11,7 +11,7 @@ import { ProductType } from "@bitwarden/common/enums";
|
||||
* @param errorMessage A localized string to display if validation fails
|
||||
* @returns A function that validates an `AbstractControl` and returns `ValidationErrors` or `null`
|
||||
*/
|
||||
export function orgWithoutAdditionalSeatLimitReachedWithUpgradePathValidator(
|
||||
export function orgSeatLimitReachedValidator(
|
||||
organization: Organization,
|
||||
allOrganizationUserEmails: string[],
|
||||
errorMessage: string,
|
@ -9,6 +9,7 @@ import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstraction
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
|
||||
|
||||
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./services/abstraction/vault-onboarding.service";
|
||||
import { VaultOnboardingComponent } from "./vault-onboarding.component";
|
||||
@ -143,7 +144,9 @@ describe("VaultOnboardingComponent", () => {
|
||||
describe("checkBrowserExtension", () => {
|
||||
it("should call getMessages when showOnboarding is true", () => {
|
||||
const messageEventSubject = new Subject<MessageEvent>();
|
||||
const messageEvent = new MessageEvent("message", { data: "hasBWInstalled" });
|
||||
const messageEvent = new MessageEvent("message", {
|
||||
data: VaultOnboardingMessages.HasBwInstalled,
|
||||
});
|
||||
const getMessagesSpy = jest.spyOn(component, "getMessages");
|
||||
|
||||
(component as any).showOnboarding = true;
|
||||
@ -151,7 +154,9 @@ describe("VaultOnboardingComponent", () => {
|
||||
messageEventSubject.next(messageEvent);
|
||||
|
||||
void fixture.whenStable().then(() => {
|
||||
expect(window.postMessage).toHaveBeenCalledWith({ command: "checkIfBWExtensionInstalled" });
|
||||
expect(window.postMessage).toHaveBeenCalledWith({
|
||||
command: VaultOnboardingMessages.checkBwInstalled,
|
||||
});
|
||||
expect(getMessagesSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -168,7 +173,7 @@ describe("VaultOnboardingComponent", () => {
|
||||
installExtension: false,
|
||||
});
|
||||
|
||||
const eventData = { data: { command: "hasBWInstalled" } };
|
||||
const eventData = { data: { command: VaultOnboardingMessages.HasBwInstalled } };
|
||||
|
||||
(component as any).showOnboarding = true;
|
||||
|
||||
|
@ -19,6 +19,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { LinkModule } from "@bitwarden/components";
|
||||
|
||||
@ -95,12 +96,12 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
|
||||
void this.getMessages(event);
|
||||
});
|
||||
|
||||
window.postMessage({ command: "checkIfBWExtensionInstalled" });
|
||||
window.postMessage({ command: VaultOnboardingMessages.checkBwInstalled });
|
||||
}
|
||||
}
|
||||
|
||||
async getMessages(event: any) {
|
||||
if (event.data.command === "hasBWInstalled" && this.showOnboarding) {
|
||||
if (event.data.command === VaultOnboardingMessages.HasBwInstalled && this.showOnboarding) {
|
||||
const currentTasks = await firstValueFrom(this.onboardingTasks$);
|
||||
const updatedTasks = {
|
||||
createAccount: currentTasks.createAccount,
|
||||
|
26
libs/auth/src/angular/anon-layout/anon-layout.component.html
Normal file
26
libs/auth/src/angular/anon-layout/anon-layout.component.html
Normal file
@ -0,0 +1,26 @@
|
||||
<main
|
||||
class="tw-flex tw-min-h-screen tw-max-w-xl tw-w-full tw-mx-auto tw-flex-col tw-gap-9 tw-px-4 tw-pb-4 tw-pt-14 tw-text-main"
|
||||
>
|
||||
<div class="tw-text-center">
|
||||
<div class="tw-px-8">
|
||||
<div *ngIf="icon" class="tw-mb-8">
|
||||
<bit-icon [icon]="icon"></bit-icon>
|
||||
</div>
|
||||
<bit-icon [icon]="logo" class="tw-mx-auto tw-block tw-max-w-72 sm:tw-max-w-xs"></bit-icon>
|
||||
</div>
|
||||
<h1 *ngIf="title" bitTypography="h3" class="tw-mt-8 sm:tw-text-2xl">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p *ngIf="subtitle" bitTypography="body1">{{ subtitle }}</p>
|
||||
</div>
|
||||
<div class="tw-mb-auto tw-mx-auto tw-max-w-md tw-grid tw-gap-9">
|
||||
<div class="tw-rounded-xl sm:tw-border sm:tw-border-solid sm:tw-border-secondary-300 sm:tw-p-8">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<ng-content select="[slot=secondary]"></ng-content>
|
||||
</div>
|
||||
<footer class="tw-text-center">
|
||||
<div>© {{ year }} Bitwarden Inc.</div>
|
||||
<div>{{ version }}</div>
|
||||
</footer>
|
||||
</main>
|
31
libs/auth/src/angular/anon-layout/anon-layout.component.ts
Normal file
31
libs/auth/src/angular/anon-layout/anon-layout.component.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { IconModule, Icon } from "../../../../components/src/icon";
|
||||
import { TypographyModule } from "../../../../components/src/typography";
|
||||
import { BitwardenLogo } from "../../icons/bitwarden-logo";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-anon-layout",
|
||||
templateUrl: "./anon-layout.component.html",
|
||||
imports: [IconModule, CommonModule, TypographyModule],
|
||||
})
|
||||
export class AnonLayoutComponent {
|
||||
@Input() title: string;
|
||||
@Input() subtitle: string;
|
||||
@Input() icon: Icon;
|
||||
|
||||
protected logo = BitwardenLogo;
|
||||
protected version: string;
|
||||
protected year = "2024";
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.year = new Date().getFullYear().toString();
|
||||
this.version = await this.platformUtilsService.getApplicationVersion();
|
||||
}
|
||||
}
|
107
libs/auth/src/angular/anon-layout/anon-layout.stories.ts
Normal file
107
libs/auth/src/angular/anon-layout/anon-layout.stories.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { ButtonModule } from "../../../../components/src/button";
|
||||
import { IconLock } from "../../icons/icon-lock";
|
||||
|
||||
import { AnonLayoutComponent } from "./anon-layout.component";
|
||||
|
||||
class MockPlatformUtilsService implements Partial<PlatformUtilsService> {
|
||||
getApplicationVersion = () => Promise.resolve("Version 2023.1.1");
|
||||
}
|
||||
|
||||
export default {
|
||||
title: "Auth/Anon Layout",
|
||||
component: AnonLayoutComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [ButtonModule],
|
||||
providers: [
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useClass: MockPlatformUtilsService,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
title: "The Page Title",
|
||||
subtitle: "The subtitle (optional)",
|
||||
icon: IconLock,
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<AnonLayoutComponent>;
|
||||
|
||||
export const WithPrimaryContent: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template:
|
||||
/**
|
||||
* The projected content (i.e. the <div> ) and styling below is just a
|
||||
* sample and could be replaced with any content and styling
|
||||
*/
|
||||
`
|
||||
<auth-anon-layout [title]="title" [subtitle]="subtitle">
|
||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
|
||||
</auth-anon-layout>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithSecondaryContent: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template:
|
||||
// Notice that slot="secondary" is requred to project any secondary content:
|
||||
`
|
||||
<auth-anon-layout [title]="title" [subtitle]="subtitle">
|
||||
<div>
|
||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
|
||||
</div>
|
||||
|
||||
<div slot="secondary" class="text-center">
|
||||
<div class="tw-font-bold tw-mb-2">Secondary Projected Content (optional)</div>
|
||||
<button bitButton>Perform Action</button>
|
||||
</div>
|
||||
</auth-anon-layout>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithLongContent: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-anon-layout title="Page Title lorem ipsum dolor consectetur sit amet expedita quod est" subtitle="Subtitle here Lorem ipsum dolor sit amet consectetur adipisicing elit. Expedita, quod est?">
|
||||
<div>
|
||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam? Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.</div>
|
||||
</div>
|
||||
|
||||
<div slot="secondary" class="text-center">
|
||||
<div class="tw-font-bold tw-mb-2">Secondary Projected Content (optional)</div>
|
||||
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Expedita, quod est? </p>
|
||||
<button bitButton>Perform Action</button>
|
||||
</div>
|
||||
</auth-anon-layout>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-anon-layout [title]="title" [subtitle]="subtitle" [icon]="icon">
|
||||
<div>
|
||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
|
||||
</div>
|
||||
</auth-anon-layout>
|
||||
`,
|
||||
}),
|
||||
};
|
9
libs/auth/src/icons/bitwarden-logo.ts
Normal file
9
libs/auth/src/icons/bitwarden-logo.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const BitwardenLogo = svgIcon`
|
||||
<svg viewBox="0 0 290 45" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Bitwarden</title>
|
||||
<path class="tw-fill-primary-500" fill-rule="evenodd" clip-rule="evenodd" d="M69.799 10.713c3.325 0 5.911 1.248 7.811 3.848 1.9 2.549 2.85 6.033 2.85 10.453 0 4.576-.95 8.113-2.902 10.61-1.953 2.547-4.592 3.743-7.918 3.743-3.325 0-5.858-1.144-7.758-3.536h-.528l-1.003 2.444a.976.976 0 0 1-.897.572H55.23a.94.94 0 0 1-.95-.936V1.352a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v8.009c0 1.144-.105 2.964-.316 5.46h.317c1.741-2.704 4.433-4.108 7.917-4.108Zm-2.428 6.084c-1.847 0-3.273.572-4.17 1.717-.844 1.144-1.32 3.068-1.32 5.668v.832c0 2.964.423 5.097 1.32 6.345.897 1.248 2.322 1.924 4.275 1.924 1.531 0 2.85-.728 3.748-2.184.897-1.404 1.372-3.537 1.372-6.189 0-2.704-.475-4.732-1.372-6.084-.95-1.352-2.27-2.029-3.853-2.029ZM93.022 38.9h-5.7a.94.94 0 0 1-.95-.936V12.221a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v25.69c.053.468-.422.988-.95.988Zm20.849-5.564c1.108 0 2.428-.208 4.011-.624a.632.632 0 0 1 .792.624v4.316a.64.64 0 0 1-.37.572c-1.794.728-4.064 1.092-6.597 1.092-3.062 0-5.278-.728-6.651-2.288-1.372-1.508-2.111-3.796-2.111-6.812V16.953h-3.008c-.37 0-.634-.26-.634-.624v-2.444c0-.052.053-.104.053-.156l4.17-2.444 2.058-5.408c.106-.26.317-.417.581-.417h3.8c.369 0 .633.26.633.625v5.252h7.548c.158 0 .317.156.317.312v4.68c0 .364-.264.624-.634.624h-7.178v13.21c0 1.04.317 1.872.897 2.34.528.572 1.373.832 2.323.832Zm35.521 5.564c-.739 0-1.319-.468-1.636-1.144l-5.595-16.797c-.369-1.196-.844-3.016-1.478-5.357h-.158l-.528 1.873-1.108 3.536-5.753 16.797c-.211.676-.845 1.092-1.584 1.092a1.628 1.628 0 0 1-1.583-1.196l-7.02-24.182c-.211-.728.369-1.508 1.214-1.508h.158c.528 0 1.003.364 1.161.884l4.117 14.717c1.003 3.849 1.689 6.657 2.006 8.53h.158c.95-3.85 1.689-6.397 2.164-7.698l5.331-15.393c.211-.624.792-1.04 1.531-1.04.686 0 1.267.416 1.478 1.04l4.961 15.29c1.214 3.9 1.953 6.396 2.217 7.696h.158c.159-1.04.792-3.952 2.006-8.633l3.958-14.509c.159-.52.634-.884 1.162-.884.791 0 1.372.728 1.161 1.508l-6.651 24.182c-.211.728-.844 1.196-1.636 1.196h-.211Zm31.352 0a.962.962 0 0 1-.95-.832l-.475-3.432h-.264c-1.372 1.716-2.745 2.964-4.223 3.692-1.425.728-3.166 1.04-5.119 1.04-2.692 0-4.751-.676-6.228-2.028-1.32-1.196-2.059-2.808-2.164-4.836-.212-2.704.95-5.305 3.166-6.813 2.27-1.456 5.437-2.34 9.712-2.34l5.173-.156v-1.768c0-2.6-.528-4.473-1.637-5.773-1.108-1.3-2.744-1.924-5.067-1.924-2.216 0-4.433.52-6.756 1.612-.58.26-1.266 0-1.53-.572s0-1.248.58-1.456c2.639-1.04 5.226-1.612 7.865-1.612 3.008 0 5.225.78 6.756 2.34 1.478 1.508 2.216 3.953 2.216 7.125v16.901c-.052.312-.527.832-1.055.832Zm-10.926-1.768c2.956 0 5.226-.832 6.862-2.444 1.689-1.612 2.533-3.952 2.533-6.813v-2.6l-4.75.208c-3.853.156-6.545.78-8.234 1.768-1.636.988-2.481 2.6-2.481 4.68 0 1.665.528 3.017 1.531 3.953 1.161.78 2.639 1.248 4.539 1.248Zm31.246-25.638c.792 0 1.584.052 2.481.156a1.176 1.176 0 0 1 1.003 1.352c-.106.624-.739.988-1.372.884-.792-.104-1.584-.208-2.375-.208-2.323 0-4.223.988-5.701 2.912-1.478 1.925-2.217 4.42-2.217 7.333v13.625c0 .676-.527 1.196-1.214 1.196-.686 0-1.213-.52-1.213-1.196V13.105c0-.572.475-1.04 1.055-1.04.581 0 1.056.416 1.056.988l.211 3.848h.158c1.109-1.976 2.323-3.38 3.589-4.16 1.214-.832 2.745-1.248 4.539-1.248Zm18.579 0c1.953 0 3.695.364 5.12 1.04 1.478.676 2.745 1.924 3.853 3.64h.158a122.343 122.343 0 0 1-.158-6.084V1.612c0-.676.528-1.196 1.214-1.196.686 0 1.214.52 1.214 1.196v36.351c0 .468-.37.832-.845.832a.852.852 0 0 1-.844-.78l-.528-3.38h-.211c-2.058 3.068-5.067 4.576-8.92 4.576-3.8 0-6.598-1.144-8.656-3.484-1.953-2.34-3.008-5.668-3.008-10.089 0-4.628.95-8.165 2.955-10.66 2.006-2.237 4.856-3.485 8.656-3.485Zm0 2.236c-3.008 0-5.225 1.04-6.756 3.12-1.478 2.029-2.216 4.993-2.216 8.945 0 7.593 3.008 11.39 9.025 11.39 3.114 0 5.331-.885 6.756-2.653 1.478-1.768 2.164-4.68 2.164-8.737v-.416c0-4.16-.686-7.124-2.164-8.893-1.372-1.872-3.642-2.756-6.809-2.756Zm31.616 25.638c-3.959 0-7.02-1.196-9.289-3.64-2.217-2.392-3.326-5.772-3.326-10.089 0-4.316 1.056-7.748 3.22-10.297 2.164-2.6 5.014-3.9 8.656-3.9 3.167 0 5.753 1.092 7.548 3.276 1.9 2.184 2.797 5.2 2.797 8.997v1.976h-19.634c.052 3.692.897 6.5 2.639 8.477 1.741 1.976 4.169 2.86 7.389 2.86 1.531 0 2.956-.104 4.117-.312.844-.156 1.847-.416 3.061-.832.686-.26 1.425.26 1.425.988 0 .416-.264.832-.686.988-1.267.52-2.481.832-3.589 1.04-1.32.364-2.745.468-4.328.468Zm-.739-25.69c-2.639 0-4.75.832-6.334 2.548-1.583 1.665-2.48 4.16-2.797 7.333h16.89c0-3.068-.686-5.564-2.059-7.28-1.372-1.717-3.272-2.6-5.7-2.6ZM288.733 38.9c-.686 0-1.214-.52-1.214-1.196V21.426c0-2.704-.58-4.68-1.689-5.877-1.214-1.196-2.955-1.872-5.383-1.872-3.273 0-5.648.78-7.126 2.444-1.478 1.613-2.322 4.265-2.322 7.853V37.6c0 .676-.528 1.196-1.214 1.196-.686 0-1.214-.52-1.214-1.196V13.105c0-.624.475-1.092 1.108-1.092.581 0 1.003.416 1.109.936l.316 2.704h.159c1.794-2.808 4.908-4.212 9.448-4.212 6.175 0 9.289 3.276 9.289 9.829V37.6c-.053.727-.633 1.3-1.267 1.3ZM90.225 0c-2.48 0-4.486 1.872-4.486 4.212v.416c0 2.289 2.058 4.213 4.486 4.213s4.486-1.924 4.486-4.213v-.364C94.711 1.872 92.653 0 90.225 0Z" />
|
||||
<path class="tw-fill-primary-500" d="M32.041 24.546V5.95H18.848v33.035c2.336-1.22 4.427-2.547 6.272-3.98 4.614-3.565 6.921-7.051 6.921-10.46Zm5.654-22.314v22.314c0 1.665-.329 3.317-.986 4.953-.658 1.637-1.473 3.09-2.445 4.359-.971 1.268-2.13 2.503-3.475 3.704-1.345 1.2-2.586 2.199-3.725 2.993a46.963 46.963 0 0 1-3.563 2.251c-1.237.707-2.116 1.187-2.636 1.439-.52.251-.938.445-1.252.58-.235.117-.49.175-.765.175s-.53-.058-.766-.174c-.314-.136-.731-.33-1.252-.581-.52-.252-1.398-.732-2.635-1.439a47.003 47.003 0 0 1-3.564-2.251c-1.138-.794-2.38-1.792-3.725-2.993-1.345-1.2-2.503-2.436-3.475-3.704-.972-1.27-1.787-2.722-2.444-4.359C.329 27.863 0 26.211 0 24.546V2.232c0-.504.187-.94.56-1.308A1.823 1.823 0 0 1 1.885.372H35.81c.511 0 .953.184 1.326.552.373.368.56.804.56 1.308Z" />
|
||||
</svg>
|
||||
`;
|
7
libs/auth/src/icons/icon-lock.ts
Normal file
7
libs/auth/src/icons/icon-lock.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const IconLock = svgIcon`
|
||||
<svg width="65" height="80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path class="tw-fill-primary-500" d="M36.554 52.684a4.133 4.133 0 0 0-.545-2.085 4.088 4.088 0 0 0-1.514-1.518 4.022 4.022 0 0 0-4.114.072 4.094 4.094 0 0 0-1.461 1.57 4.153 4.153 0 0 0 .175 4.16c.393.616.94 1.113 1.588 1.44v6.736a1.864 1.864 0 0 0 .498 1.365c.17.18.376.328.603.425a1.781 1.781 0 0 0 1.437 0c.227-.097.432-.242.603-.425a1.864 1.864 0 0 0 .499-1.365v-6.745a4.05 4.05 0 0 0 1.62-1.498c.392-.64.604-1.377.611-2.132ZM57.86 25.527h-2.242c-.175 0-.35-.037-.514-.105a1.3 1.3 0 0 1-.434-.297 1.379 1.379 0 0 1-.39-.963v-1a23 23 0 0 0-5.455-15.32A22.46 22.46 0 0 0 34.673.101a21.633 21.633 0 0 0-8.998 1.032 21.777 21.777 0 0 0-7.813 4.637 22.118 22.118 0 0 0-5.286 7.446 22.376 22.376 0 0 0-1.855 8.975v1.62c0 .03-.118 1.705-1.555 1.73h-2.02A6.723 6.723 0 0 0 2.37 27.56 6.887 6.887 0 0 0 .4 32.403V73.12a6.905 6.905 0 0 0 1.97 4.847A6.76 6.76 0 0 0 7.146 80h50.713a6.746 6.746 0 0 0 4.77-2.03 6.925 6.925 0 0 0 1.971-4.845V32.403a6.91 6.91 0 0 0-1.965-4.85 6.793 6.793 0 0 0-2.19-1.493 6.676 6.676 0 0 0-2.588-.53l.002-.003Zm-42.2-3.335c-.007-2.55.549-5.07 1.625-7.373a17.085 17.085 0 0 1 4.606-5.945 16.8 16.8 0 0 1 6.684-3.358 16.71 16.71 0 0 1 7.462-.115c3.835.91 7.245 3.12 9.665 6.266a17.61 17.61 0 0 1 3.64 11.02v1.475c0 .18-.035.358-.102.523a1.349 1.349 0 0 1-1.244.842H17.722a1.876 1.876 0 0 1-.744-.085 1.894 1.894 0 0 1-1.119-.957 1.98 1.98 0 0 1-.204-.728v-1.565h.005ZM59.663 73.12c0 .487-.19.952-.529 1.3a1.796 1.796 0 0 1-1.279.545H7.146a1.826 1.826 0 0 1-1.807-1.845V32.403a1.85 1.85 0 0 1 .523-1.3c.168-.17.365-.308.585-.4.22-.093.454-.14.691-.143h50.719c.479.005.938.2 1.276.545.339.345.526.81.526 1.295v40.717l.003.003Z" />
|
||||
</svg>
|
||||
`;
|
@ -37,6 +37,10 @@ export function canAccessBillingTab(org: Organization): boolean {
|
||||
}
|
||||
|
||||
export function canAccessOrgAdmin(org: Organization): boolean {
|
||||
// Admin console can only be accessed by Owners for disabled organizations
|
||||
if (!org.enabled && !org.isOwner) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
canAccessMembersTab(org) ||
|
||||
canAccessGroupsTab(org) ||
|
||||
|
@ -17,63 +17,70 @@ import { StateDefinition } from "./state-definition";
|
||||
*
|
||||
*/
|
||||
|
||||
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||
|
||||
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
||||
export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory");
|
||||
|
||||
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
||||
|
||||
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
|
||||
|
||||
export const VAULT_ONBOARDING = new StateDefinition("vaultOnboarding", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
|
||||
export const GENERATOR_DISK = new StateDefinition("generator", "disk");
|
||||
export const GENERATOR_MEMORY = new StateDefinition("generator", "memory");
|
||||
|
||||
export const BIOMETRIC_SETTINGS_DISK = new StateDefinition("biometricSettings", "disk");
|
||||
|
||||
// Admin Console
|
||||
|
||||
export const ORGANIZATIONS_DISK = new StateDefinition("organizations", "disk");
|
||||
export const POLICIES_DISK = new StateDefinition("policies", "disk");
|
||||
export const PROVIDERS_DISK = new StateDefinition("providers", "disk");
|
||||
|
||||
export const BILLING_DISK = new StateDefinition("billing", "disk");
|
||||
// Auth
|
||||
|
||||
export const FOLDER_DISK = new StateDefinition("folder", "disk", { web: "memory" });
|
||||
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
||||
|
||||
export const VAULT_SETTINGS_DISK = new StateDefinition("vaultSettings", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
|
||||
export const COLLECTION_DATA = new StateDefinition("collection", "disk", {
|
||||
web: "memory",
|
||||
});
|
||||
// Autofill
|
||||
|
||||
export const BADGE_SETTINGS_DISK = new StateDefinition("badgeSettings", "disk");
|
||||
export const USER_NOTIFICATION_SETTINGS_DISK = new StateDefinition(
|
||||
"userNotificationSettings",
|
||||
"disk",
|
||||
);
|
||||
|
||||
// Billing
|
||||
|
||||
export const AUTOFILL_SETTINGS_DISK = new StateDefinition("autofillSettings", "disk");
|
||||
export const AUTOFILL_SETTINGS_DISK_LOCAL = new StateDefinition("autofillSettingsLocal", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
export const BILLING_DISK = new StateDefinition("billing", "disk");
|
||||
|
||||
export const VAULT_FILTER_DISK = new StateDefinition("vaultFilter", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
|
||||
export const USER_NOTIFICATION_SETTINGS_DISK = new StateDefinition(
|
||||
"userNotificationSettings",
|
||||
"disk",
|
||||
);
|
||||
export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk");
|
||||
// Components
|
||||
|
||||
export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanner", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
|
||||
// Platform
|
||||
|
||||
export const BIOMETRIC_SETTINGS_DISK = new StateDefinition("biometricSettings", "disk");
|
||||
export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk");
|
||||
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
||||
export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory");
|
||||
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
|
||||
|
||||
// Secrets Manager
|
||||
|
||||
export const SM_ONBOARDING_DISK = new StateDefinition("smOnboarding", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
|
||||
// Tools
|
||||
|
||||
export const GENERATOR_DISK = new StateDefinition("generator", "disk");
|
||||
export const GENERATOR_MEMORY = new StateDefinition("generator", "memory");
|
||||
|
||||
// Vault
|
||||
|
||||
export const COLLECTION_DATA = new StateDefinition("collection", "disk", {
|
||||
web: "memory",
|
||||
});
|
||||
export const FOLDER_DISK = new StateDefinition("folder", "disk", { web: "memory" });
|
||||
export const VAULT_FILTER_DISK = new StateDefinition("vaultFilter", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
export const VAULT_ONBOARDING = new StateDefinition("vaultOnboarding", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
export const VAULT_SETTINGS_DISK = new StateDefinition("vaultSettings", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
|
@ -16,6 +16,10 @@ export interface NewCredentialParams {
|
||||
* Whether or not the user must be verified before completing the operation.
|
||||
*/
|
||||
userVerification: boolean;
|
||||
/**
|
||||
* The relying party ID is usually the URL
|
||||
*/
|
||||
rpId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
6
libs/common/src/vault/enums/vault-onboarding.enum.ts
Normal file
6
libs/common/src/vault/enums/vault-onboarding.enum.ts
Normal file
@ -0,0 +1,6 @@
|
||||
const VaultOnboardingMessages = {
|
||||
HasBwInstalled: "hasBwInstalled",
|
||||
checkBwInstalled: "checkIfBWExtensionInstalled",
|
||||
} as const;
|
||||
|
||||
export { VaultOnboardingMessages };
|
@ -216,6 +216,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
credentialName: params.rpEntity.name,
|
||||
userName: params.userEntity.displayName,
|
||||
userVerification,
|
||||
rpId: params.rpEntity.id,
|
||||
} as NewCredentialParams);
|
||||
});
|
||||
}
|
||||
|
@ -113,6 +113,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
credentialName: params.rpEntity.name,
|
||||
userName: params.userEntity.displayName,
|
||||
userVerification: params.requireUserVerification,
|
||||
rpId: params.rpEntity.id,
|
||||
});
|
||||
const cipherId = response.cipherId;
|
||||
userVerified = response.userVerified;
|
||||
|
@ -8,7 +8,11 @@ function rgba(color) {
|
||||
|
||||
module.exports = {
|
||||
prefix: "tw-",
|
||||
content: ["./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}"],
|
||||
content: [
|
||||
"./src/**/*.{html,ts}",
|
||||
"../../libs/components/src/**/*.{html,ts}",
|
||||
"../../libs/auth/src/**/*.{html,ts}",
|
||||
],
|
||||
safelist: [],
|
||||
corePlugins: { preflight: false },
|
||||
theme: {
|
||||
|
2
package-lock.json
generated
2
package-lock.json
generated
@ -264,7 +264,7 @@
|
||||
},
|
||||
"apps/web": {
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2024.2.4"
|
||||
"version": "2024.2.5"
|
||||
},
|
||||
"libs/admin-console": {
|
||||
"name": "@bitwarden/admin-console",
|
||||
|
Loading…
Reference in New Issue
Block a user