Skip to content

Commit

Permalink
chore: add option to transition your data - internal feature
Browse files Browse the repository at this point in the history
  • Loading branch information
karolsojko committed Aug 24, 2023
1 parent fcaaf54 commit 6f0742d
Show file tree
Hide file tree
Showing 13 changed files with 249 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
MfaServiceInterface,
GenerateUuid,
CreateDecryptedBackupFile,
GetTransitionStatus,
StartTransition,
} from '@standardnotes/services'
import { VaultLockServiceInterface } from './../VaultLock/VaultLockServiceInterface'
import { HistoryServiceInterface } from './../History/HistoryServiceInterface'
Expand Down Expand Up @@ -76,6 +78,8 @@ export interface ApplicationInterface {
get generateUuid(): GenerateUuid
get getHost(): GetHost
get setHost(): SetHost
get getTransitionStatus(): GetTransitionStatus
get startTransition(): StartTransition

// Services
get alerts(): AlertService
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum InternalFeature {
Vaults = 'vaults',
HomeServer = 'home-server',
Transition = 'transition',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { HttpServiceInterface } from '@standardnotes/api'

import { GetTransitionStatus } from './GetTransitionStatus'

describe('GetTransitionStatus', () => {
let httpService: HttpServiceInterface

const createUseCase = () => new GetTransitionStatus(httpService)

beforeEach(() => {
httpService = {
get: jest.fn(),
} as unknown as HttpServiceInterface
})

it('should get transition status', async () => {
const useCase = createUseCase()
;(httpService.get as jest.Mock).mockResolvedValueOnce({ status: 200, data: { status: 'TO-DO' } })

const result = await useCase.execute()

expect(result.isFailed()).toBe(false)
expect(result.getValue).toBe('TO-DO')
})

it('should fail to get transition status', async () => {
const useCase = createUseCase()
;(httpService.get as jest.Mock).mockResolvedValueOnce({ status: 400 })

const result = await useCase.execute()

expect(result.isFailed()).toBe(true)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { HttpServiceInterface } from '@standardnotes/api'
import { HttpStatusCode } from '@standardnotes/responses'

export class GetTransitionStatus implements UseCaseInterface<'TO-DO' | 'STARTED' | 'FINISHED' | 'FAILED'> {
constructor(private httpService: HttpServiceInterface) {}

async execute(): Promise<Result<'TO-DO' | 'STARTED' | 'FINISHED' | 'FAILED'>> {
const response = await this.httpService.get('/v1/users/transition-status')

if (response.status !== HttpStatusCode.Success) {
return Result.fail('Failed to get transition status')
}

return Result.ok((response.data as { status: 'TO-DO' | 'STARTED' | 'FINISHED' | 'FAILED' }).status)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { HttpServiceInterface } from '@standardnotes/api'

import { StartTransition } from './StartTransition'

describe('StartTransition', () => {
let httpService: HttpServiceInterface

const createUseCase = () => new StartTransition(httpService)

beforeEach(() => {
httpService = {
post: jest.fn(),
} as unknown as HttpServiceInterface
})

it('should start transition', async () => {
const useCase = createUseCase()
;(httpService.post as jest.Mock).mockResolvedValueOnce({ status: 200 })

const result = await useCase.execute()

expect(result.isFailed()).toBe(false)
})

it('should fail to start transition', async () => {
const useCase = createUseCase()
;(httpService.post as jest.Mock).mockResolvedValueOnce({ status: 400 })

const result = await useCase.execute()

expect(result.isFailed()).toBe(true)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { HttpServiceInterface } from '@standardnotes/api'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { HttpStatusCode } from '@standardnotes/responses'

export class StartTransition implements UseCaseInterface<void> {
constructor(private httpService: HttpServiceInterface) {}

async execute(): Promise<Result<void>> {
const response = await this.httpService.post('/v1/items/transition')

if (response.status !== HttpStatusCode.Success) {
return Result.fail('Failed to start transition')
}

return Result.ok()
}
}
2 changes: 2 additions & 0 deletions packages/services/src/Domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ export * from './Sync/SyncOptions'
export * from './Sync/SyncQueueStrategy'
export * from './Sync/SyncServiceInterface'
export * from './Sync/SyncSource'
export * from './UseCase/Transition/GetTransitionStatus/GetTransitionStatus'
export * from './UseCase/Transition/StartTransition/StartTransition'
export * from './UseCase/ChangeAndSaveItem'
export * from './UseCase/DiscardItemsLocally'
export * from './UseCase/GenerateUuid'
Expand Down
10 changes: 10 additions & 0 deletions packages/snjs/lib/Application/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ import {
GenerateUuid,
CreateDecryptedBackupFile,
CreateEncryptedBackupFile,
GetTransitionStatus,
StartTransition,
} from '@standardnotes/services'
import {
SNNote,
Expand Down Expand Up @@ -1138,6 +1140,14 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.dependencies.get<SetHost>(TYPES.SetHost)
}

get getTransitionStatus(): GetTransitionStatus {
return this.dependencies.get<GetTransitionStatus>(TYPES.GetTransitionStatus)
}

get startTransition(): StartTransition {
return this.dependencies.get<StartTransition>(TYPES.StartTransition)
}

public get legacyApi(): LegacyApiService {
return this.dependencies.get<LegacyApiService>(TYPES.LegacyApiService)
}
Expand Down
11 changes: 11 additions & 0 deletions packages/snjs/lib/Application/Dependencies/Dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ import {
CreateDecryptedBackupFile,
CreateEncryptedBackupFile,
SyncLocalVaultsWithRemoteSharedVaults,
GetTransitionStatus,
StartTransition,
} from '@standardnotes/services'
import { ItemManager } from '../../Services/Items/ItemManager'
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
Expand All @@ -153,6 +155,7 @@ import {
AuthenticatorApiService,
AuthenticatorServer,
HttpService,
HttpServiceInterface,
RevisionApiService,
RevisionServer,
SharedVaultInvitesServer,
Expand Down Expand Up @@ -1023,6 +1026,14 @@ export class Dependencies {
)
})

this.factory.set(TYPES.GetTransitionStatus, () => {
return new GetTransitionStatus(this.get<HttpServiceInterface>(TYPES.HttpService))
})

this.factory.set(TYPES.StartTransition, () => {
return new StartTransition(this.get<HttpServiceInterface>(TYPES.HttpService))
})

this.factory.set(TYPES.ListRevisions, () => {
return new ListRevisions(this.get<RevisionManager>(TYPES.RevisionManager))
})
Expand Down
2 changes: 2 additions & 0 deletions packages/snjs/lib/Application/Dependencies/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ export const TYPES = {
AuthorizeVaultDeletion: Symbol.for('AuthorizeVaultDeletion'),
CreateDecryptedBackupFile: Symbol.for('CreateDecryptedBackupFile'),
CreateEncryptedBackupFile: Symbol.for('CreateEncryptedBackupFile'),
GetTransitionStatus: Symbol.for('GetTransitionStatus'),
StartTransition: Symbol.for('StartTransition'),

// Mappers
SessionStorageMapper: Symbol.for('SessionStorageMapper'),
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/javascripts/Application/DevMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export class DevMode {
constructor(private application: WebApplicationInterface) {
InternalFeatureService.get().enableFeature(InternalFeature.Vaults)
InternalFeatureService.get().enableFeature(InternalFeature.HomeServer)
InternalFeatureService.get().enableFeature(InternalFeature.Transition)
}

/** Valid only when running a mock event publisher on port 3124 */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,91 @@
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
import { observer } from 'mobx-react-lite'

import { Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
import Button from '@/Components/Button/Button'
import { SyncQueueStrategy } from '@standardnotes/snjs'
import { STRING_GENERIC_SYNC_ERROR } from '@/Constants/Strings'
import { observer } from 'mobx-react-lite'
import { WebApplication } from '@/Application/WebApplication'
import { FunctionComponent, useState } from 'react'
import { formatLastSyncDate } from '@/Utils/DateUtils'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import { featureTrunkTransitionEnabled } from '@/FeatureTrunk'

type Props = {
application: WebApplication
}

const Sync: FunctionComponent<Props> = ({ application }: Props) => {
const TRANSITION_STATUS_REFRESH_INTERVAL = 5000

const [isSyncingInProgress, setIsSyncingInProgress] = useState(false)
const [isTransitionInProgress, setIsTransitionInProgress] = useState(false)
const [showTransitionSegment, setShowTransitionSegment] = useState(false)
const [transitionStatus, setTransitionStatus] = useState('')
const [transitionStatusIntervalRef, setTransitionStatusIntervalRef] = useState<NodeJS.Timer | null>(null)
const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.sync.getLastSyncDate() as Date))

const setupTransitionStatusRefresh = useCallback(async () => {
const interval = setInterval(async () => {
const statusOrError = await application.getTransitionStatus.execute()
if (statusOrError.isFailed()) {
await application.alerts.alert(statusOrError.getError())
return
}
const status = statusOrError.getValue()

setTransitionStatus(status)
}, TRANSITION_STATUS_REFRESH_INTERVAL)

setTransitionStatusIntervalRef(interval)
}, [application, setTransitionStatus, setTransitionStatusIntervalRef])

useEffect(() => {
if (!featureTrunkTransitionEnabled()) {
return
}

async function checkTransitionStatus() {
const statusOrError = await application.getTransitionStatus.execute()
if (statusOrError.isFailed()) {
await application.alerts.alert(statusOrError.getError())
return
}
const status = statusOrError.getValue()

if (status === 'FINISHED') {
if (transitionStatusIntervalRef) {
clearInterval(transitionStatusIntervalRef)
}
setIsTransitionInProgress(false)
setTransitionStatus(status)
setShowTransitionSegment(false)

return
}

setShowTransitionSegment(true)
setTransitionStatus(status)

if (status === 'STARTED') {
setIsTransitionInProgress(true)
if (!transitionStatusIntervalRef) {
await setupTransitionStatusRefresh()
}
}
}

void checkTransitionStatus()
}, [
application,
setIsTransitionInProgress,
setTransitionStatus,
setShowTransitionSegment,
setupTransitionStatusRefresh,
transitionStatusIntervalRef,
])

const doSynchronization = async () => {
setIsSyncingInProgress(true)

Expand All @@ -32,6 +101,19 @@ const Sync: FunctionComponent<Props> = ({ application }: Props) => {
}
}

const doTransition = useCallback(async () => {
const resultOrError = await application.startTransition.execute()
if (resultOrError.isFailed()) {
await application.alerts.alert(resultOrError.getError())

return
}

setIsTransitionInProgress(true)

await setupTransitionStatusRefresh()
}, [application, setupTransitionStatusRefresh])

return (
<PreferencesGroup>
<PreferencesSegment>
Expand All @@ -50,6 +132,35 @@ const Sync: FunctionComponent<Props> = ({ application }: Props) => {
</div>
</div>
</PreferencesSegment>
{showTransitionSegment && (
<>
<HorizontalSeparator classes="my-4" />
<PreferencesSegment>
<div className="flex flex-row items-center">
<div className="flex flex-grow flex-col">
<Title>Transition Account</Title>
<Text>
Transition your account to our new infrastructure in order to enable new features and improve your
overall experience. Depending on the amount of data you have, this process may take a few moments.
</Text>
{isTransitionInProgress && (
<Text>
<span className="font-bold">Transition status:</span> {transitionStatus}
</Text>
)}
{!isTransitionInProgress && (
<Button
className="mt-3 min-w-20"
label="Start transition"
disabled={isTransitionInProgress}
onClick={doTransition}
/>
)}
</div>
</div>
</PreferencesSegment>
</>
)}
</PreferencesGroup>
)
}
Expand Down
4 changes: 4 additions & 0 deletions packages/web/src/javascripts/FeatureTrunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ export function featureTrunkVaultsEnabled(): boolean {
export function featureTrunkHomeServerEnabled(): boolean {
return InternalFeatureService.get().isFeatureEnabled(InternalFeature.HomeServer)
}

export function featureTrunkTransitionEnabled(): boolean {
return InternalFeatureService.get().isFeatureEnabled(InternalFeature.Transition)
}

0 comments on commit 6f0742d

Please sign in to comment.