Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

chore: add server switcher #2717

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -64,7 +64,7 @@ export interface ApplicationInterface {
setPreference<K extends PrefKey>(key: K, value: PrefValue[K]): Promise<void>

hasAccount(): boolean
setCustomHost(host: string): Promise<void>
setCustomHost(host: string, websocketUrl?: string): Promise<void>
isUsingHomeServer(): Promise<boolean>

importData(data: BackupFile, awaitSync?: boolean): Promise<Result<ImportDataResult>>
Expand Down
4 changes: 2 additions & 2 deletions packages/snjs/lib/Application/Application.ts
Expand Up @@ -576,10 +576,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return compareVersions(userVersion, ProtocolVersion.V004) >= 0
}

public async setCustomHost(host: string): Promise<void> {
public async setCustomHost(host: string, websocketUrl?: string): Promise<void> {
await this.setHost.execute(host)

this.sockets.setWebSocketUrl(undefined)
this.sockets.setWebSocketUrl(websocketUrl)
}

public getUserPasswordCreationDate(): Date | undefined {
Expand Down
1 change: 1 addition & 0 deletions packages/snjs/lib/Application/Dependencies/Dependencies.ts
Expand Up @@ -1528,6 +1528,7 @@ export class Dependencies {
this.get<HttpService>(TYPES.HttpService),
this.get<DiskStorageService>(TYPES.DiskStorageService),
this.options.defaultHost,
this.options.identifier,
this.get<InMemoryStore>(TYPES.InMemoryStore),
this.get<PureCryptoInterface>(TYPES.Crypto),
this.get<SessionStorageMapper>(TYPES.SessionStorageMapper),
Expand Down
27 changes: 27 additions & 0 deletions packages/snjs/lib/Migrations/Versions/2_204_8.ts
@@ -0,0 +1,27 @@
import { ApplicationStage, StorageKey } from '@standardnotes/services'
import { Migration } from '@Lib/Migrations/Migration'

export class Migration2_204_8 extends Migration {
static override version(): string {
return '2.204.8'
}

protected registerStageHandlers(): void {
this.registerStageHandler(ApplicationStage.Launched_10, async () => {
await this.migrateHostKeyStoredToWorkspaceIdentified()

this.markDone()
})
}

private async migrateHostKeyStoredToWorkspaceIdentified(): Promise<void> {
const existingHostKeyValue = this.services.storageService.getValue<string | undefined>(StorageKey.ServerHost)
if (existingHostKeyValue === undefined) {
return
}

this.services.storageService.setValue(`${StorageKey.ServerHost}:${this.services.identifier}`, existingHostKeyValue)

await this.services.storageService.removeValue(StorageKey.ServerHost)
}
}
7 changes: 5 additions & 2 deletions packages/snjs/lib/Services/Api/ApiService.ts
Expand Up @@ -106,6 +106,7 @@ export class LegacyApiService
private httpService: HttpServiceInterface,
private storageService: DiskStorageService,
private host: string,
private workspaceIdentifier: string,
private inMemoryStore: KeyValueStoreInterface<string>,
private crypto: PureCryptoInterface,
private sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>,
Expand Down Expand Up @@ -142,14 +143,16 @@ export class LegacyApiService
}

public loadHost(): string {
const storedValue = this.storageService.getValue<string | undefined>(StorageKey.ServerHost)
const storedValue = this.storageService.getValue<string | undefined>(
`${StorageKey.ServerHost}:${this.workspaceIdentifier}`,
)
this.host = storedValue || this.host
return this.host
}

public async setHost(host: string): Promise<void> {
this.host = host
this.storageService.setValue(StorageKey.ServerHost, host)
this.storageService.setValue(`${StorageKey.ServerHost}:${this.workspaceIdentifier}`, host)
}

public getHost(): string {
Expand Down
2 changes: 1 addition & 1 deletion packages/snjs/lib/Services/Session/SessionManager.ts
Expand Up @@ -169,7 +169,7 @@ export class SessionManager
}
}

const serverHost = this.storage.getValue<string>(StorageKey.ServerHost)
const serverHost = this.storage.getValue<string | undefined>(`${StorageKey.ServerHost}:${this.workspaceIdentifier}`)
if (serverHost) {
void this.apiService.setHost(serverHost)
this.httpService.setHost(serverHost)
Expand Down
4 changes: 4 additions & 0 deletions packages/snjs/lib/Url/DefaultHost.ts
@@ -0,0 +1,4 @@
export enum DefaultHost {
Api = 'https://api.standardnotes.com',
WebSocket = 'wss://sockets.standardnotes.com',
}
1 change: 1 addition & 0 deletions packages/snjs/lib/Url/index.ts
@@ -0,0 +1 @@
export * from './DefaultHost'
1 change: 1 addition & 0 deletions packages/snjs/lib/index.ts
Expand Up @@ -7,6 +7,7 @@ export * from './Log'
export * from './Migrations'
export * from './Services'
export * from './Types'
export * from './Url'
export * from './Version'
export { KeyParamsOrigination } from '@standardnotes/common'
export * from '@standardnotes/domain-core'
Expand Down
@@ -1,9 +1,10 @@
import { observer } from 'mobx-react-lite'
import { ChangeEventHandler, FunctionComponent, ReactNode, useCallback, useEffect, useState } from 'react'
import { FunctionComponent, ReactNode, useCallback, useEffect, useState } from 'react'
import Checkbox from '@/Components/Checkbox/Checkbox'
import DecoratedInput from '@/Components/Input/DecoratedInput'
import Icon from '@/Components/Icon/Icon'
import { useApplication } from '../ApplicationProvider'
import ServerPicker from './ServerPicker/ServerPicker'

type Props = {
disabled?: boolean
Expand All @@ -22,7 +23,7 @@ const AdvancedOptions: FunctionComponent<Props> = ({
}) => {
const application = useApplication()

const { server, setServer, enableServerOption, setEnableServerOption } = application.accountMenuController
const { server, setServer } = application.accountMenuController
const [showAdvanced, setShowAdvanced] = useState(false)

const [isPrivateUsername, setIsPrivateUsername] = useState(false)
Expand Down Expand Up @@ -71,9 +72,8 @@ const AdvancedOptions: FunctionComponent<Props> = ({
if (!isRecoveryCodes) {
setIsPrivateUsername(false)
setIsStrictSignin(false)
setEnableServerOption(false)
}
}, [isRecoveryCodes, setIsPrivateUsername, setIsStrictSignin, setEnableServerOption, onRecoveryCodesChange])
}, [isRecoveryCodes, setIsPrivateUsername, setIsStrictSignin, onRecoveryCodesChange])

const handleRecoveryCodesChange = useCallback(
(recoveryCodes: string) => {
Expand All @@ -85,19 +85,10 @@ const AdvancedOptions: FunctionComponent<Props> = ({
[onRecoveryCodesChange],
)

const handleServerOptionChange: ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
if (e.target instanceof HTMLInputElement) {
setEnableServerOption(e.target.checked)
}
},
[setEnableServerOption],
)

const handleSyncServerChange = useCallback(
(server: string) => {
(server: string, websocketUrl?: string) => {
setServer(server)
application.setCustomHost(server).catch(console.error)
application.setCustomHost(server, websocketUrl).catch(console.error)
},
[application, setServer],
)
Expand All @@ -124,100 +115,87 @@ const AdvancedOptions: FunctionComponent<Props> = ({
</div>
</button>
{showAdvanced ? (
<div className="my-2 px-3">
{children}

<div className="mb-1 flex items-center justify-between">
<Checkbox
name="private-workspace"
label="Private username mode"
checked={isPrivateUsername}
disabled={disabled || isRecoveryCodes}
onChange={handleIsPrivateUsernameChange}
/>
<a href="https://standardnotes.com/help/80" target="_blank" rel="noopener noreferrer" title="Learn more">
<Icon type="info" className="text-neutral" />
</a>
</div>

{isPrivateUsername && (
<>
<DecoratedInput
className={{ container: 'mb-2' }}
left={[<Icon type="account-circle" className="text-neutral" />]}
type="text"
placeholder="Username"
value={privateUsername}
onChange={handlePrivateUsernameNameChange}
disabled={disabled || isRecoveryCodes}
spellcheck={false}
autocomplete={false}
/>
</>
)}
<>
<div className="my-2 px-3">
{children}

{onStrictSignInChange && (
<div className="mb-1 flex items-center justify-between">
<Checkbox
name="use-strict-signin"
label="Use strict sign-in"
checked={isStrictSignin}
name="private-workspace"
label="Private username mode"
checked={isPrivateUsername}
disabled={disabled || isRecoveryCodes}
onChange={handleStrictSigninChange}
onChange={handleIsPrivateUsernameChange}
/>
<a
href="https://standardnotes.com/help/security"
target="_blank"
rel="noopener noreferrer"
title="Learn more"
>
<a href="https://standardnotes.com/help/80" target="_blank" rel="noopener noreferrer" title="Learn more">
<Icon type="info" className="text-neutral" />
</a>
</div>
)}

<div className="mb-1 flex items-center justify-between">
<Checkbox
name="recovery-codes"
label="Use recovery code"
checked={isRecoveryCodes}
disabled={disabled}
onChange={handleIsRecoveryCodesChange}
/>
</div>

{isRecoveryCodes && (
<>
<DecoratedInput
className={{ container: 'mb-2' }}
left={[<Icon type="security" className="text-neutral" />]}
type="text"
placeholder="Recovery code"
value={recoveryCodes}
onChange={handleRecoveryCodesChange}
{isPrivateUsername && (
<>
<DecoratedInput
className={{ container: 'mb-2' }}
left={[<Icon type="account-circle" className="text-neutral" />]}
type="text"
placeholder="Username"
value={privateUsername}
onChange={handlePrivateUsernameNameChange}
disabled={disabled || isRecoveryCodes}
spellcheck={false}
autocomplete={false}
/>
</>
)}

{onStrictSignInChange && (
<div className="mb-1 flex items-center justify-between">
<Checkbox
name="use-strict-signin"
label="Use strict sign-in"
checked={isStrictSignin}
disabled={disabled || isRecoveryCodes}
onChange={handleStrictSigninChange}
/>
<a
href="https://standardnotes.com/help/security"
target="_blank"
rel="noopener noreferrer"
title="Learn more"
>
<Icon type="info" className="text-neutral" />
</a>
</div>
)}

<div className="mb-1 flex items-center justify-between">
<Checkbox
name="recovery-codes"
label="Use recovery code"
checked={isRecoveryCodes}
disabled={disabled}
spellcheck={false}
autocomplete={false}
onChange={handleIsRecoveryCodesChange}
/>
</>
)}

<Checkbox
name="custom-sync-server"
label="Custom sync server"
checked={enableServerOption}
onChange={handleServerOptionChange}
disabled={disabled || isRecoveryCodes}
/>
<DecoratedInput
type="text"
left={[<Icon type="server" className="text-neutral" />]}
placeholder="https://api.standardnotes.com"
value={server}
onChange={handleSyncServerChange}
disabled={!enableServerOption && !disabled && !isRecoveryCodes}
/>
</div>
</div>

{isRecoveryCodes && (
<>
<DecoratedInput
className={{ container: 'mb-2' }}
left={[<Icon type="security" className="text-neutral" />]}
type="text"
placeholder="Recovery code"
value={recoveryCodes}
onChange={handleRecoveryCodesChange}
disabled={disabled}
spellcheck={false}
autocomplete={false}
/>
</>
)}
</div>
<ServerPicker customServerAddress={server} handleCustomServerAddressChange={handleSyncServerChange} />
</>
) : null}
</>
)
Expand Down