From d9f27eb771e431310806325150de476d07673582 Mon Sep 17 00:00:00 2001 From: Mo Date: Tue, 4 Jul 2023 08:20:56 -0500 Subject: [PATCH 01/10] fix: dynamically handle trusted contact changes and update UI --- .../src/Domain/Contacts/ContactService.ts | 2 +- .../Contacts/Managers/SelfContactManager.ts | 56 ++++++++++--------- .../Domain/SharedVaults/SharedVaultService.ts | 29 ++-------- .../Vaults/Invites/GreenCheckmarkCircle.tsx | 9 +++ .../Panes/Vaults/Invites/InviteItem.tsx | 32 +++++++---- .../Preferences/Panes/Vaults/Vaults.tsx | 33 ++++++----- .../Vaults/VaultModal/VaultModalInvites.tsx | 3 +- 7 files changed, 84 insertions(+), 80 deletions(-) create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/GreenCheckmarkCircle.tsx diff --git a/packages/services/src/Domain/Contacts/ContactService.ts b/packages/services/src/Domain/Contacts/ContactService.ts index 0d139583a97..2a0aea17dac 100644 --- a/packages/services/src/Domain/Contacts/ContactService.ts +++ b/packages/services/src/Domain/Contacts/ContactService.ts @@ -237,7 +237,7 @@ export class ContactService } findTrustedContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined { - return this.findTrustedContact(invite.user_uuid) + return this.findTrustedContact(invite.sender_uuid) } getCollaborationIDForTrustedContact(contact: TrustedContactInterface): string { diff --git a/packages/services/src/Domain/Contacts/Managers/SelfContactManager.ts b/packages/services/src/Domain/Contacts/Managers/SelfContactManager.ts index ace8c11fe34..4b3dfa0e22e 100644 --- a/packages/services/src/Domain/Contacts/Managers/SelfContactManager.ts +++ b/packages/services/src/Domain/Contacts/Managers/SelfContactManager.ts @@ -21,7 +21,7 @@ import { PublicKeySet } from '@standardnotes/encryption' export class SelfContactManager { public selfContact?: TrustedContactInterface - private shouldReloadSelfContact = true + private isReloadingSelfContact = false private eventDisposers: (() => void)[] = [] @@ -32,16 +32,14 @@ export class SelfContactManager { private session: SessionsClientInterface, private singletons: SingletonManagerInterface, ) { - this.eventDisposers.push( - items.addObserver(ContentType.TrustedContact, () => { - this.shouldReloadSelfContact = true - }), - ) - this.eventDisposers.push( sync.addEventObserver((event) => { - if (event === SyncEvent.SyncCompletedWithAllItemsUploaded || event === SyncEvent.LocalDataIncrementalLoad) { - void this.reloadSelfContact() + if (event === SyncEvent.LocalDataIncrementalLoad) { + this.loadSelfContactFromDatabase() + } + + if (event === SyncEvent.SyncCompletedWithAllItemsUploaded) { + void this.reloadSelfContactAndCreateIfNecessary() } }), ) @@ -49,11 +47,19 @@ export class SelfContactManager { public async handleApplicationStage(stage: ApplicationStage): Promise { if (stage === ApplicationStage.LoadedDatabase_12) { - this.selfContact = this.singletons.findSingleton( - ContentType.UserPrefs, - TrustedContact.singletonPredicate, - ) + this.loadSelfContactFromDatabase() + } + } + + private loadSelfContactFromDatabase(): void { + if (this.selfContact) { + return } + + this.selfContact = this.singletons.findSingleton( + ContentType.TrustedContact, + TrustedContact.singletonPredicate, + ) } public async updateWithNewPublicKeySet(publicKeySet: PublicKeySet) { @@ -74,12 +80,16 @@ export class SelfContactManager { }) } - private async reloadSelfContact() { + private async reloadSelfContactAndCreateIfNecessary() { if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) { return } - if (!this.shouldReloadSelfContact || this.isReloadingSelfContact) { + if (this.selfContact) { + return + } + + if (this.isReloadingSelfContact) { return } @@ -105,17 +115,13 @@ export class SelfContactManager { }), } - try { - this.selfContact = await this.singletons.findOrCreateSingleton( - TrustedContact.singletonPredicate, - ContentType.TrustedContact, - FillItemContent(content), - ) + this.selfContact = await this.singletons.findOrCreateSingleton( + TrustedContact.singletonPredicate, + ContentType.TrustedContact, + FillItemContent(content), + ) - this.shouldReloadSelfContact = false - } finally { - this.isReloadingSelfContact = false - } + this.isReloadingSelfContact = false } deinit() { diff --git a/packages/services/src/Domain/SharedVaults/SharedVaultService.ts b/packages/services/src/Domain/SharedVaults/SharedVaultService.ts index ae3356ce7d3..da20007311a 100644 --- a/packages/services/src/Domain/SharedVaults/SharedVaultService.ts +++ b/packages/services/src/Domain/SharedVaults/SharedVaultService.ts @@ -111,7 +111,9 @@ export class SharedVaultService ) this.eventDisposers.push( - items.addObserver(ContentType.TrustedContact, ({ changed, inserted, source }) => { + items.addObserver(ContentType.TrustedContact, async ({ changed, inserted, source }) => { + await this.reprocessCachedInvitesTrustStatusAfterTrustedContactsChange() + if (source === PayloadEmitSource.LocalChanged && inserted.length > 0) { void this.handleCreationOfNewTrustedContacts(inserted) } @@ -250,8 +252,6 @@ export class SharedVaultService } private async handleTrustedContactsChange(contacts: TrustedContactInterface[]): Promise { - await this.reprocessCachedInvitesTrustStatusAfterTrustedContactsChange() - for (const contact of contacts) { await this.shareContactWithUserAdministeredSharedVaults(contact) } @@ -328,28 +328,9 @@ export class SharedVaultService } private async reprocessCachedInvitesTrustStatusAfterTrustedContactsChange(): Promise { - const cachedInvites = this.getCachedPendingInviteRecords() - - for (const record of cachedInvites) { - if (record.trusted) { - continue - } + const cachedInvites = this.getCachedPendingInviteRecords().map((record) => record.invite) - const trustedMessageUseCase = new GetAsymmetricMessageTrustedPayload( - this.encryption, - this.contacts, - ) - - const trustedMessage = trustedMessageUseCase.execute({ - message: record.invite, - privateKey: this.encryption.getKeyPair().privateKey, - }) - - if (trustedMessage) { - record.message = trustedMessage - record.trusted = true - } - } + await this.processInboundInvites(cachedInvites) } private async processInboundInvites(invites: SharedVaultInviteServerHash[]): Promise { diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/GreenCheckmarkCircle.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/GreenCheckmarkCircle.tsx new file mode 100644 index 00000000000..d6779596ce2 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/GreenCheckmarkCircle.tsx @@ -0,0 +1,9 @@ +import Icon from '@/Components/Icon/Icon' + +export const GreenCheckmarkCircle = () => { + return ( + + ) +} diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/InviteItem.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/InviteItem.tsx index 4e77932b7c7..a55150911fa 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/InviteItem.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/InviteItem.tsx @@ -5,33 +5,36 @@ import ModalOverlay from '@/Components/Modal/ModalOverlay' import { PendingSharedVaultInviteRecord } from '@standardnotes/snjs' import { useCallback, useState } from 'react' import EditContactModal from '../Contacts/EditContactModal' +import { GreenCheckmarkCircle } from './GreenCheckmarkCircle' type Props = { - invite: PendingSharedVaultInviteRecord + inviteRecord: PendingSharedVaultInviteRecord } -const InviteItem = ({ invite }: Props) => { +const InviteItem = ({ inviteRecord }: Props) => { const application = useApplication() const [isAddContactModalOpen, setIsAddContactModalOpen] = useState(false) - const isTrusted = invite.trusted - const inviteData = invite.message.data + const isTrusted = inviteRecord.trusted + const inviteData = inviteRecord.message.data const addAsTrustedContact = useCallback(() => { setIsAddContactModalOpen(true) }, []) const acceptInvite = useCallback(async () => { - await application.sharedVaults.acceptPendingSharedVaultInvite(invite) - }, [application.sharedVaults, invite]) + await application.sharedVaults.acceptPendingSharedVaultInvite(inviteRecord) + }, [application.sharedVaults, inviteRecord]) const closeAddContactModal = () => setIsAddContactModalOpen(false) - const collaborationId = application.contacts.getCollaborationIDFromInvite(invite.invite) + const collaborationId = application.contacts.getCollaborationIDFromInvite(inviteRecord.invite) + + const trustedContact = application.contacts.findTrustedContactForInvite(inviteRecord.invite) return ( <> - +
@@ -41,9 +44,16 @@ const InviteItem = ({ invite }: Props) => { Vault Description: {inviteData.metadata.description} - - Sender CollaborationID: {collaborationId} - + {trustedContact ? ( +
+ Trusted Sender: {trustedContact.name} + +
+ ) : ( + + Sender CollaborationID: {collaborationId} + + )}
{isTrusted ? ( diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx index bb0a9b671da..17b8d629fb8 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx @@ -40,31 +40,36 @@ const Vaults = () => { setVaults(vaultService.getVaults()) }, [vaultService]) - const fetchInvites = useCallback(async () => { - await sharedVaultService.downloadInboundInvites() - const invites = sharedVaultService.getCachedPendingInviteRecords() - setInvites(invites) + const updateInvites = useCallback(async () => { + setInvites(sharedVaultService.getCachedPendingInviteRecords()) }, [sharedVaultService]) const updateContacts = useCallback(async () => { setContacts(contactService.getAllContacts()) }, [contactService]) + const updateAllData = useCallback(async () => { + await Promise.all([updateVaults(), updateInvites(), updateContacts()]) + }, [updateContacts, updateInvites, updateVaults]) + useEffect(() => { return application.sharedVaults.addEventObserver((event) => { if (event === SharedVaultServiceEvent.SharedVaultStatusChanged) { - void fetchInvites() + void updateAllData() } }) - }) + }, [application.sharedVaults, updateAllData]) useEffect(() => { return application.streamItems([ContentType.VaultListing, ContentType.TrustedContact], () => { - void updateVaults() - void fetchInvites() - void updateContacts() + void updateAllData() }) - }, [application, updateVaults, fetchInvites, updateContacts]) + }, [application, updateAllData]) + + useEffect(() => { + void sharedVaultService.downloadInboundInvites() + void updateAllData() + }, [updateAllData, sharedVaultService]) const createNewVault = useCallback(async () => { setIsVaultModalOpen(true) @@ -74,12 +79,6 @@ const Vaults = () => { setIsAddContactModalOpen(true) }, []) - useEffect(() => { - void updateVaults() - void fetchInvites() - void updateContacts() - }, [updateContacts, updateVaults, fetchInvites]) - return ( <> @@ -95,7 +94,7 @@ const Vaults = () => { Incoming Invites
{invites.map((invite) => { - return + return })}
diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalInvites.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalInvites.tsx index d0df96dda2b..4a97ecece9e 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalInvites.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalInvites.tsx @@ -28,9 +28,8 @@ export const VaultModalInvites = ({
Pending Invites
{invites.map((invite) => { const contact = application.contacts.findTrustedContactForInvite(invite) - return ( -
+
From 877ba550c7fd3f9afb2aa87b9fd717b99da79b06 Mon Sep 17 00:00:00 2001 From: Mo Date: Tue, 4 Jul 2023 08:23:06 -0500 Subject: [PATCH 02/10] fix: contact uuid --- .../Preferences/Panes/Vaults/Contacts/ContactItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/ContactItem.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/ContactItem.tsx index 948c0fc86bd..ab63d9c9ecb 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/ContactItem.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/ContactItem.tsx @@ -25,7 +25,7 @@ const ContactItem = ({ contact }: Props) => { return ( <> - +
From 9c1f46895cb9ae1606389ae0e1d00d6110932a19 Mon Sep 17 00:00:00 2001 From: Mo Date: Wed, 5 Jul 2023 07:42:35 -0500 Subject: [PATCH 03/10] feat: change password wizard preprocessing step --- .../AsymmetricMessagePayloadType.ts | 7 +- .../Application/ApplicationInterface.ts | 3 + .../AsymmetricMessageService.ts | 19 ++- .../AsymmetricMessageServiceInterface.ts | 7 + packages/services/src/Domain/index.ts | 1 + packages/snjs/lib/Application/Application.ts | 4 + .../Components/PasswordWizard/FinishStep.tsx | 17 +++ .../PasswordWizard/PasswordStep.tsx | 75 ++++++++++ .../PasswordWizard/PasswordWizard.tsx | 136 +++++++++--------- .../PasswordWizard/PreprocessingStep.tsx | 97 +++++++++++++ .../Vaults/Invites/GreenCheckmarkCircle.tsx | 6 +- .../Components/Preferences/PreferencesMenu.ts | 2 - 12 files changed, 302 insertions(+), 72 deletions(-) create mode 100644 packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageServiceInterface.ts create mode 100644 packages/web/src/javascripts/Components/PasswordWizard/FinishStep.tsx create mode 100644 packages/web/src/javascripts/Components/PasswordWizard/PasswordStep.tsx create mode 100644 packages/web/src/javascripts/Components/PasswordWizard/PreprocessingStep.tsx diff --git a/packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessagePayloadType.ts b/packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessagePayloadType.ts index c4dab81c131..5b4f200c57f 100644 --- a/packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessagePayloadType.ts +++ b/packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessagePayloadType.ts @@ -2,6 +2,11 @@ export enum AsymmetricMessagePayloadType { ContactShare = 'contact-share', SharedVaultRootKeyChanged = 'shared-vault-root-key-changed', SenderKeypairChanged = 'sender-keypair-changed', - SharedVaultInvite = 'shared-vault-invite', SharedVaultMetadataChanged = 'shared-vault-metadata-changed', + + /** + * Shared Vault Invites conform to the asymmetric message protocol, but are sent via the dedicated + * SharedVaultInvite model and not the AsymmetricMessage model on the server side. + */ + SharedVaultInvite = 'shared-vault-invite', } diff --git a/packages/services/src/Domain/Application/ApplicationInterface.ts b/packages/services/src/Domain/Application/ApplicationInterface.ts index 9cc18aeef99..35cad8ce1e8 100644 --- a/packages/services/src/Domain/Application/ApplicationInterface.ts +++ b/packages/services/src/Domain/Application/ApplicationInterface.ts @@ -1,3 +1,4 @@ +import { AsymmetricMessageServiceInterface } from './../AsymmetricMessage/AsymmetricMessageServiceInterface' import { SyncOptions } from './../Sync/SyncOptions' import { ImportDataReturnType } from './../Mutator/ImportDataUseCase' import { ChallengeServiceInterface } from './../Challenge/ChallengeServiceInterface' @@ -102,6 +103,8 @@ export interface ApplicationInterface { get vaults(): VaultServiceInterface get challenges(): ChallengeServiceInterface get alerts(): AlertService + get asymmetric(): AsymmetricMessageServiceInterface + readonly identifier: ApplicationIdentifier readonly platform: Platform deviceInterface: DeviceInterface diff --git a/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.ts b/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.ts index 40d1f2fc710..f7609b37427 100644 --- a/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.ts +++ b/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.ts @@ -1,6 +1,6 @@ import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' import { ContactServiceInterface } from './../Contacts/ContactServiceInterface' -import { AsymmetricMessageServerHash, ClientDisplayableError } from '@standardnotes/responses' +import { AsymmetricMessageServerHash, ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses' import { SyncEvent, SyncEventReceivedAsymmetricMessagesData } from '../Event/SyncEvent' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface' @@ -27,8 +27,12 @@ import { SendOwnContactChangeMessage } from './UseCase/SendOwnContactChangeMessa import { GetOutboundAsymmetricMessages } from './UseCase/GetOutboundAsymmetricMessages' import { GetInboundAsymmetricMessages } from './UseCase/GetInboundAsymmetricMessages' import { GetVaultUseCase } from '../Vaults/UseCase/GetVault' +import { AsymmetricMessageServiceInterface } from './AsymmetricMessageServiceInterface' -export class AsymmetricMessageService extends AbstractService implements InternalEventHandlerInterface { +export class AsymmetricMessageService + extends AbstractService + implements AsymmetricMessageServiceInterface, InternalEventHandlerInterface +{ private messageServer: AsymmetricMessageServer constructor( @@ -69,7 +73,16 @@ export class AsymmetricMessageService extends AbstractService implements Interna return usecase.execute() } - async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise { + public async downloadAndProcessInboundMessages(): Promise { + const messages = await this.getInboundMessages() + if (isClientDisplayableError(messages)) { + return + } + + await this.handleRemoteReceivedAsymmetricMessages(messages) + } + + private async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise { if (!data.oldKeyPair || !data.oldSigningKeyPair) { return } diff --git a/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageServiceInterface.ts b/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageServiceInterface.ts new file mode 100644 index 00000000000..aef70743816 --- /dev/null +++ b/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageServiceInterface.ts @@ -0,0 +1,7 @@ +import { AsymmetricMessageServerHash, ClientDisplayableError } from '@standardnotes/responses' + +export interface AsymmetricMessageServiceInterface { + getOutboundMessages(): Promise + getInboundMessages(): Promise + downloadAndProcessInboundMessages(): Promise +} diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index b48fa7e2b0a..26002e9876e 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -9,6 +9,7 @@ export * from './Application/DeinitMode' export * from './Application/DeinitSource' export * from './AsymmetricMessage/AsymmetricMessageService' +export * from './AsymmetricMessage/AsymmetricMessageServiceInterface' export * from './Auth/AuthClientInterface' export * from './Auth/AuthManager' diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index d1cbeb0a01a..b5bbf5924db 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -387,6 +387,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.challengeService } + public get asymmetric(): ExternalServices.AsymmetricMessageServiceInterface { + return this.asymmetricMessageService + } + get homeServer(): ExternalServices.HomeServerServiceInterface | undefined { return this.homeServerService } diff --git a/packages/web/src/javascripts/Components/PasswordWizard/FinishStep.tsx b/packages/web/src/javascripts/Components/PasswordWizard/FinishStep.tsx new file mode 100644 index 00000000000..d5b350deace --- /dev/null +++ b/packages/web/src/javascripts/Components/PasswordWizard/FinishStep.tsx @@ -0,0 +1,17 @@ +import { GreenCheckmarkCircle } from '../Preferences/Panes/Vaults/Invites/GreenCheckmarkCircle' + +export const FinishStep = () => { + return ( +
+
+
+ +
+
+
Your password has been successfully changed.
+

Ensure you are running the latest version of Standard Notes on all platforms for maximum compatibility.

+
+
+
+ ) +} diff --git a/packages/web/src/javascripts/Components/PasswordWizard/PasswordStep.tsx b/packages/web/src/javascripts/Components/PasswordWizard/PasswordStep.tsx new file mode 100644 index 00000000000..89bd666c841 --- /dev/null +++ b/packages/web/src/javascripts/Components/PasswordWizard/PasswordStep.tsx @@ -0,0 +1,75 @@ +import { useState } from 'react' +import DecoratedPasswordInput from '../Input/DecoratedPasswordInput' + +export const PasswordStep = ({ + onCurrentPasswordChange, + onNewPasswordChange, + onNewPasswordConfirmationChange, +}: { + onCurrentPasswordChange: (value: string) => void + onNewPasswordChange: (value: string) => void + onNewPasswordConfirmationChange: (value: string) => void +}) => { + const [currentPassword, setCurrentPassword] = useState('') + const [newPassword, setNewPassword] = useState('') + const [newPasswordConfirmation, setNewPasswordConfirmation] = useState('') + + const handleCurrentPasswordChange = (value: string) => { + setCurrentPassword(value) + onCurrentPasswordChange(value) + } + + const handleNewPasswordChange = (value: string) => { + setNewPassword(value) + onNewPasswordChange(value) + } + + const handleNewPasswordConfirmationChange = (value: string) => { + setNewPasswordConfirmation(value) + onNewPasswordConfirmationChange(value) + } + + return ( +
+
+ + + + +
+ + + + + +
+ + + + + +
+ ) +} diff --git a/packages/web/src/javascripts/Components/PasswordWizard/PasswordWizard.tsx b/packages/web/src/javascripts/Components/PasswordWizard/PasswordWizard.tsx index a4caf536383..81e4ae2f498 100644 --- a/packages/web/src/javascripts/Components/PasswordWizard/PasswordWizard.tsx +++ b/packages/web/src/javascripts/Components/PasswordWizard/PasswordWizard.tsx @@ -1,10 +1,13 @@ import { WebApplication } from '@/Application/WebApplication' -import { createRef } from 'react' import { AbstractComponent } from '@/Components/Abstract/PureComponent' -import DecoratedPasswordInput from '../Input/DecoratedPasswordInput' import Modal from '../Modal/Modal' import { isMobileScreen } from '@/Utils' import Spinner from '../Spinner/Spinner' +import { PasswordStep } from './PasswordStep' +import { FinishStep } from './FinishStep' +import { PreprocessingStep } from './PreprocessingStep' +import { InternalFeatureService } from '@standardnotes/snjs' +import { featureTrunkVaultsEnabled } from '@/FeatureTrunk' interface Props { application: WebApplication @@ -19,7 +22,6 @@ type State = { processing?: boolean showSpinner?: boolean step: Steps - title: string } const DEFAULT_CONTINUE_TITLE = 'Continue' @@ -27,8 +29,9 @@ const GENERATING_CONTINUE_TITLE = 'Generating Keys...' const FINISH_CONTINUE_TITLE = 'Finish' enum Steps { - PasswordStep = 1, - FinishStep = 2, + PreprocessingStep = 'preprocessing-step', + PasswordStep = 'password-step', + FinishStep = 'finish-step', } type FormData = { @@ -39,22 +42,32 @@ type FormData = { } class PasswordWizard extends AbstractComponent { - private currentPasswordInput = createRef() - constructor(props: Props) { super(props, props.application) this.registerWindowUnloadStopper() - this.state = { + + const baseState = { formData: {}, continueTitle: DEFAULT_CONTINUE_TITLE, - step: Steps.PasswordStep, - title: 'Change Password', + } + + if (featureTrunkVaultsEnabled()) { + this.state = { + ...baseState, + lockContinue: true, + step: Steps.PreprocessingStep, + } + } else { + this.state = { + ...baseState, + lockContinue: false, + step: Steps.PasswordStep, + } } } override componentDidMount(): void { super.componentDidMount() - this.currentPasswordInput.current?.focus() } override componentWillUnmount(): void { @@ -83,6 +96,15 @@ class PasswordWizard extends AbstractComponent { if (this.state.step === Steps.FinishStep) { this.dismiss() + + return + } + + if (this.state.step === Steps.PreprocessingStep) { + this.setState({ + step: Steps.PasswordStep, + }) + return } @@ -142,7 +164,6 @@ class PasswordWizard extends AbstractComponent { return false } - /** Validate current password */ const success = await this.application.validateAccountPassword(this.state.formData.currentPassword as string) if (!success) { this.application.alertService @@ -192,7 +213,7 @@ class PasswordWizard extends AbstractComponent { } dismiss = () => { - if (this.state.lockContinue) { + if (this.state.processing) { this.application.alertService.alert('Cannot close window until pending tasks are complete.').catch(console.error) } else { this.props.dismissModal() @@ -226,11 +247,32 @@ class PasswordWizard extends AbstractComponent { }).catch(console.error) } + setContinueEnabled = (enabled: boolean) => { + this.setState({ + lockContinue: !enabled, + }) + } + + nextStepFromPreprocessing = () => { + if (this.state.lockContinue) { + this.setState( + { + lockContinue: false, + }, + () => { + void this.nextStep() + }, + ) + } else { + void this.nextStep() + } + } + override render() { return (
{ }, ]} > -
- {this.state.step === Steps.PasswordStep && ( -
-
- - - - -
- - - - - -
- - - - - -
+
+ {this.state.step === Steps.PreprocessingStep && ( + )} - {this.state.step === Steps.FinishStep && ( -
-
Your password has been successfully changed.
-

- Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum - compatibility. -

-
+ + {this.state.step === Steps.PasswordStep && ( + )} + + {this.state.step === Steps.FinishStep && }
diff --git a/packages/web/src/javascripts/Components/PasswordWizard/PreprocessingStep.tsx b/packages/web/src/javascripts/Components/PasswordWizard/PreprocessingStep.tsx new file mode 100644 index 00000000000..5b76e3773d9 --- /dev/null +++ b/packages/web/src/javascripts/Components/PasswordWizard/PreprocessingStep.tsx @@ -0,0 +1,97 @@ +import Spinner from '../Spinner/Spinner' +import { useApplication } from '../ApplicationProvider' +import { useCallback, useEffect, useState } from 'react' + +export const PreprocessingStep = ({ + onContinue, + setContinueEnabled, +}: { + onContinue: () => void + setContinueEnabled: (disabled: boolean) => void +}) => { + const application = useApplication() + + const [isProcessingSync, setIsProcessingSync] = useState(true) + const [isProcessingMessages, setIsProcessingMessages] = useState(true) + const [isProcessingInvites, setIsProcessingInvites] = useState(true) + const [needsUserConfirmation, setNeedsUserConfirmation] = useState<'yes' | 'no'>() + + const continueIfPossible = useCallback(() => { + if (isProcessingMessages || isProcessingInvites || isProcessingSync) { + setContinueEnabled(false) + return + } + + if (needsUserConfirmation === 'yes') { + setContinueEnabled(true) + return + } + + onContinue() + }, [ + isProcessingInvites, + isProcessingMessages, + isProcessingSync, + needsUserConfirmation, + onContinue, + setContinueEnabled, + ]) + + useEffect(() => { + continueIfPossible() + }, [isProcessingInvites, isProcessingMessages, isProcessingSync, continueIfPossible]) + + useEffect(() => { + const processPendingSync = async () => { + await application.sync.sync() + setIsProcessingSync(false) + } + + void processPendingSync() + }, [application.sync]) + + useEffect(() => { + const processPendingMessages = async () => { + await application.asymmetric.downloadAndProcessInboundMessages() + setIsProcessingMessages(false) + } + + void processPendingMessages() + }, [application.asymmetric]) + + useEffect(() => { + const processPendingInvites = async () => { + await application.sharedVaults.downloadInboundInvites() + const hasPendingInvites = application.sharedVaults.getCachedPendingInviteRecords().length > 0 + setNeedsUserConfirmation(hasPendingInvites ? 'yes' : 'no') + setIsProcessingInvites(false) + } + + void processPendingInvites() + }, [application.sharedVaults]) + + const isProcessing = isProcessingSync || isProcessingMessages || isProcessingInvites + + if (isProcessing) { + return ( +
+ +

Checking for data conflicts...

+
+ ) + } + + if (needsUserConfirmation === 'no') { + return null + } + + return ( +
+

+ You have pending vault invites. Changing your password will delete these invites. It is recommended you accept + or decline these invites before changing your password. If you choose to continue, these invites will be + deleted. +

+
+ ) +} diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/GreenCheckmarkCircle.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/GreenCheckmarkCircle.tsx index d6779596ce2..bd651e25f8e 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/GreenCheckmarkCircle.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/GreenCheckmarkCircle.tsx @@ -2,7 +2,11 @@ import Icon from '@/Components/Icon/Icon' export const GreenCheckmarkCircle = () => { return ( - ) diff --git a/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts b/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts index f54e1f67cbb..d1c474ab4ec 100644 --- a/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts +++ b/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts @@ -72,8 +72,6 @@ export class PreferencesMenu { this._menu = menuItems.sort((a, b) => a.order - b.order) - this._menu = this._enableUnfinishedFeatures ? PREFERENCES_MENU_ITEMS : READY_PREFERENCES_MENU_ITEMS - this.loadLatestVersions() makeAutoObservable< From f343335fbfb418a7aae2c6436f29ef1034250d07 Mon Sep 17 00:00:00 2001 From: Mo Date: Wed, 5 Jul 2023 07:43:01 -0500 Subject: [PATCH 04/10] chore: lint --- .../src/javascripts/Components/PasswordWizard/PasswordWizard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/web/src/javascripts/Components/PasswordWizard/PasswordWizard.tsx b/packages/web/src/javascripts/Components/PasswordWizard/PasswordWizard.tsx index 81e4ae2f498..ec90f6ad316 100644 --- a/packages/web/src/javascripts/Components/PasswordWizard/PasswordWizard.tsx +++ b/packages/web/src/javascripts/Components/PasswordWizard/PasswordWizard.tsx @@ -6,7 +6,6 @@ import Spinner from '../Spinner/Spinner' import { PasswordStep } from './PasswordStep' import { FinishStep } from './FinishStep' import { PreprocessingStep } from './PreprocessingStep' -import { InternalFeatureService } from '@standardnotes/snjs' import { featureTrunkVaultsEnabled } from '@/FeatureTrunk' interface Props { From ab7a3b40cac431479ac5ea237a35c834369448c4 Mon Sep 17 00:00:00 2001 From: Mo Date: Thu, 6 Jul 2023 06:46:48 -0500 Subject: [PATCH 05/10] style: use icon for security error --- .../Components/PasswordWizard/FinishStep.tsx | 4 ++-- .../Preferences/Panes/Security/ErroredItems.tsx | 12 ++++++------ .../Preferences/Panes/Vaults/Invites/InviteItem.tsx | 4 ++-- .../Preferences/PreferencesComponents/MenuItem.tsx | 7 ++++++- .../CheckmarkCircle.tsx} | 2 +- .../Components/UIElements/ErrorCircle.tsx | 13 +++++++++++++ 6 files changed, 30 insertions(+), 12 deletions(-) rename packages/web/src/javascripts/Components/{Preferences/Panes/Vaults/Invites/GreenCheckmarkCircle.tsx => UIElements/CheckmarkCircle.tsx} (86%) create mode 100644 packages/web/src/javascripts/Components/UIElements/ErrorCircle.tsx diff --git a/packages/web/src/javascripts/Components/PasswordWizard/FinishStep.tsx b/packages/web/src/javascripts/Components/PasswordWizard/FinishStep.tsx index d5b350deace..34489550399 100644 --- a/packages/web/src/javascripts/Components/PasswordWizard/FinishStep.tsx +++ b/packages/web/src/javascripts/Components/PasswordWizard/FinishStep.tsx @@ -1,11 +1,11 @@ -import { GreenCheckmarkCircle } from '../Preferences/Panes/Vaults/Invites/GreenCheckmarkCircle' +import { CheckmarkCircle } from '../UIElements/CheckmarkCircle' export const FinishStep = () => { return (
- +
Your password has been successfully changed.
diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/ErroredItems.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/ErroredItems.tsx index 8fddeed725b..4da6db4cc1f 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Security/ErroredItems.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/ErroredItems.tsx @@ -12,6 +12,7 @@ import Button from '@/Components/Button/Button' import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' +import { ErrorCircle } from '@/Components/UIElements/ErrorCircle' type Props = { viewControllerManager: ViewControllerManager } @@ -66,8 +67,9 @@ const ErroredItems: FunctionComponent = ({ viewControllerManager }: Props return ( - - Error decrypting items <span className="ml-1 text-warning">⚠️</span> + <Title className="flex flex-row items-center gap-2"> + <ErrorCircle /> + Error decrypting items {`${erroredItems.length} items are errored and could not be decrypted.`}
@@ -95,10 +97,8 @@ const ErroredItems: FunctionComponent = ({ viewControllerManager }: Props
{`${getContentTypeDisplay(item)} created on ${item.createdAtString}`} - -
Item ID: {item.uuid}
-
Last Modified: {item.updatedAtString}
-
+ Item ID: {item.uuid} + Last Modified: {item.updatedAtString}
+ ) +} From 40b479057f5dfc4779302e457d59b1a02114485c Mon Sep 17 00:00:00 2001 From: Mo Date: Thu, 6 Jul 2023 06:55:11 -0500 Subject: [PATCH 06/10] fix: allow key recovery of root key encrypted items --- .../KeyRecovery/KeyRecoveryService.ts | 5 +++ .../Components/Preferences/PaneSelector.tsx | 4 +-- .../Panes/Security/ErroredItems.tsx | 36 ++++++++++--------- .../Preferences/Panes/Security/Security.tsx | 4 +-- .../Preferences/PreferencesCanvas.tsx | 4 +-- ...esMenu.ts => PreferencesMenuController.ts} | 4 +-- .../Preferences/PreferencesMenuView.tsx | 4 +-- .../Preferences/PreferencesView.tsx | 4 +-- 8 files changed, 36 insertions(+), 29 deletions(-) rename packages/web/src/javascripts/Components/Preferences/{PreferencesMenu.ts => PreferencesMenuController.ts} (98%) diff --git a/packages/snjs/lib/Services/KeyRecovery/KeyRecoveryService.ts b/packages/snjs/lib/Services/KeyRecovery/KeyRecoveryService.ts index a41c21a2a7f..8b44a488629 100644 --- a/packages/snjs/lib/Services/KeyRecovery/KeyRecoveryService.ts +++ b/packages/snjs/lib/Services/KeyRecovery/KeyRecoveryService.ts @@ -9,6 +9,7 @@ import { PayloadEmitSource, EncryptedItemInterface, getIncrementedDirtyIndex, + ContentTypeUsesRootKeyEncryption, } from '@standardnotes/models' import { SNSyncService } from '../Sync/SyncService' import { DiskStorageService } from '../Storage/DiskStorageService' @@ -187,6 +188,10 @@ export class SNKeyRecoveryService extends AbstractService = ({ +const PaneSelector: FunctionComponent = ({ menu, viewControllerManager, application, diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/ErroredItems.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/ErroredItems.tsx index 4da6db4cc1f..1251c4382ea 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Security/ErroredItems.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/ErroredItems.tsx @@ -1,10 +1,10 @@ -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { observer } from 'mobx-react-lite' -import { Fragment, FunctionComponent, useState } from 'react' +import { Fragment, FunctionComponent, useEffect, useState } from 'react' import { Text, Title, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content' import { ButtonType, ClientDisplayableError, + ContentType, DisplayStringForContentType, EncryptedItemInterface, } from '@standardnotes/snjs' @@ -13,13 +13,17 @@ import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' import { ErrorCircle } from '@/Components/UIElements/ErrorCircle' +import { useApplication } from '@/Components/ApplicationProvider' -type Props = { viewControllerManager: ViewControllerManager } +const ErroredItems: FunctionComponent = () => { + const application = useApplication() + const [erroredItems, setErroredItems] = useState(application.items.invalidNonVaultedItems) -const ErroredItems: FunctionComponent = ({ viewControllerManager }: Props) => { - const app = viewControllerManager.application - - const [erroredItems, setErroredItems] = useState(app.items.invalidNonVaultedItems) + useEffect(() => { + return application.streamItems(ContentType.Any, () => { + setErroredItems(application.items.invalidNonVaultedItems) + }) + }, [application]) const getContentTypeDisplay = (item: EncryptedItemInterface): string => { const display = DisplayStringForContentType(item.content_type) @@ -35,7 +39,7 @@ const ErroredItems: FunctionComponent = ({ viewControllerManager }: Props } const deleteItems = async (items: EncryptedItemInterface[]): Promise => { - const confirmed = await app.alertService.confirm( + const confirmed = await application.alertService.confirm( `Are you sure you want to permanently delete ${items.length} item(s)?`, undefined, 'Delete', @@ -45,23 +49,23 @@ const ErroredItems: FunctionComponent = ({ viewControllerManager }: Props return } - void app.mutator.deleteItems(items).then(() => { - void app.sync.sync() + void application.mutator.deleteItems(items).then(() => { + void application.sync.sync() }) - setErroredItems(app.items.invalidItems) + setErroredItems(application.items.invalidItems) } const attemptDecryption = (item: EncryptedItemInterface): void => { - const errorOrTrue = app.canAttemptDecryptionOfItem(item) + const errorOrTrue = application.canAttemptDecryptionOfItem(item) if (errorOrTrue instanceof ClientDisplayableError) { - void app.alertService.showErrorAlert(errorOrTrue) + void application.alertService.showErrorAlert(errorOrTrue) return } - app.presentKeyRecoveryWizard() + application.presentKeyRecoveryWizard() } return ( @@ -77,7 +81,7 @@ const ErroredItems: FunctionComponent = ({ viewControllerManager }: Props className="mt-3 mr-2 min-w-20" label="Export all" onClick={() => { - void app.getArchiveService().downloadEncryptedItems(erroredItems) + void application.getArchiveService().downloadEncryptedItems(erroredItems) }} />