Compare commits

...
Author SHA1 Message Date
StandardNotes CI 17458b61d1 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected]
2024-04-10 10:57:00 +00:00
Aman Harwara 03da45537d chore: add proton theme [skip e2e] 2024-04-10 16:18:01 +05:30
StandardNotes CI 7332f10ff4 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected]
2024-04-08 16:02:49 +00:00
Mo b6eda707bd chore: auth verification (#2867) [skip e2e] 2024-04-08 21:22:56 +05:30
StandardNotes CI a37e095907 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2024-04-04 11:31:41 +00:00
Aman Harwara ef81e9d97e chore: fix super block dragging indicator line not disappearing correctly (#2866) [skip e2e] 2024-04-04 16:53:02 +05:30
Aman Harwara c6a31f209d chore: fix tags list not scrolling correctly when dragging a tag (#2865) 2024-04-04 16:52:41 +05:30
StandardNotes CI 292153333f chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].9
2024-04-01 14:12:16 +00:00
Aman Harwara 6cef772212 refactor: link editor [skip e2e] 2024-04-01 19:33:12 +05:30
Aman Harwara f4ab9b0d97 chore: fix keyboard nav on super toolbar [skip e2e] 2024-04-01 18:26:49 +05:30
Aman Harwara 73eba38c14 fix: Fixed super note "Table of contents" not working on mobile 2024-04-01 17:34:03 +05:30
Micah Zoltu 4b19076f9b chore: fix webapp build instructions (#2856) 2024-03-31 15:43:09 +05:30
StandardNotes CI da9c412703 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].8
2024-03-28 08:36:52 +00:00
Aman Harwara 9784ff0173 chore: fix PDF export CSP error on webapp [skip e2e] 2024-03-28 13:57:49 +05:30
StandardNotes CI d8df19b0bd chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].7
2024-03-27 16:11:01 +00:00
Aman Harwara da16a779ce chore: dedupe embedded file ids when exporting a super note [skip e2e] 2024-03-27 21:31:56 +05:30
StandardNotes CI 473fb6be53 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected].6
2024-03-26 15:48:01 +00:00
Aman Harwara d5cac8e32d chore: trigger release [skip e2e] 2024-03-26 21:08:45 +05:30
Aman Harwara 45ca99c627 fix: Fixed issue with system color scheme on mobile 2024-03-26 21:08:04 +05:30
StandardNotes CI 8a6e123fbc chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
2024-03-26 13:35:04 +00:00
Karol Sójko 914c444349 chore(web): remove unused setting for failed email backups - skip e2e (#2863)
* chore(web): remove unused setting for failed email backups

* adjust tests
2024-03-26 14:25:19 +01:00
StandardNotes CI bf8ad837ef chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected].4
2024-03-22 19:21:42 +00:00
Aman Harwara e3ae421f7c chore: fix issues with multiple non-layerable themes being active on color scheme change [skip e2e] 2024-03-23 00:43:02 +05:30
Aman Harwara a162294d04 chore: fix csp errors in dev server 2024-03-23 00:25:02 +05:30
Aman Harwara f0613e7af5 chore: fix theme not toggling when changing auto-light/dark theme selection 2024-03-23 00:24:41 +05:30
Aman Harwara 7967ddead7 fix: Fixed issue with system color scheme not being applied correctly on start 2024-03-23 00:01:54 +05:30
StandardNotes CI f868989bb5 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
2024-03-22 13:59:33 +00:00
Aman Harwara eb5e5e11e0 chore: fix dark mode keyboard shortcut [skip e2e] 2024-03-22 19:19:23 +05:30
StandardNotes CI 0d1831ea49 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2024-03-22 08:55:23 +00:00
Karol Sójko a22000242a chore: block disabling 2FA if U2F authenticators present - skip e2e (#2862)
* chore: block disabling 2FA if U2F authenticators present

* chore: remove the u2f wrapper component

* chore: adjust fetching 2fa status

* chore: fix property name

* chore: remove wrapper
2024-03-22 09:45:15 +01:00
Karol Sójko 4068d5b480 chore(web): prevent from adding U2F if 2FA is not enabled (#2861) 2024-03-20 15:31:40 +01:00
StandardNotes CI fd0d08c153 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2024-03-14 15:03:24 +00:00
Aman Harwara adef74f23f chore: release [skip e2e] 2024-03-14 20:24:30 +05:30
Aman Harwara b90c725f96 chore: remove unused patch [skip e2] 2024-03-14 20:13:15 +05:30
Aman Harwara 74b459cae0 chore: update changelog [skip e2e] 2024-03-14 20:12:23 +05:30
Aman Harwara c1e9a7fe40 chore: revert changes [skip e2e] 2024-03-14 20:11:17 +05:30
Aman Harwara edf8de4468 Revert "chore: sync lexical with patch [skip e2e]"
This reverts commit 45c661c136.
2024-03-14 20:10:55 +05:30
Aman Harwara bb81c8acfc Revert "feat: Markdown text is now parsed correctly when pasted"
This reverts commit c5ca4a9857.
2024-03-14 20:10:35 +05:30
StandardNotes CI 414a2dda0b chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2024-03-14 13:47:55 +00:00
Aman Harwara 45c661c136 chore: sync lexical with patch [skip e2e] 2024-03-14 19:09:00 +05:30
Aman Harwara c5ca4a9857 feat: Markdown text is now parsed correctly when pasted 2024-03-14 19:01:39 +05:30
StandardNotes CI 56813909bb chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
2024-03-14 10:21:01 +00:00
Karol Sójkoandmoughxyz 7a4172ad11 chore: add e2e test for refresh tokens cooldown period - skip e2e (#2860)
* chore: add e2e test for refresh tokens cooldown period

* chore: fix refreshing session in e2e

* chore: fix session refresh cooldown test

* chore: fix e2e test

* Add dropped response simulation test

---------

Co-authored-by: moughxyz <[email protected]>
2024-03-14 11:11:17 +01:00
StandardNotes CI e97e788b5f chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
2024-03-13 18:48:39 +00:00
Aman Harwara 20a9ef0dad chore: stable scrollbar gutters on windows [skip e2e] 2024-03-14 00:09:50 +05:30
Aman Harwara 670f27b9cd chore: update changelog [skip ci] 2024-03-12 19:41:34 +05:30
Aman Harwara f45d854006 chore: update changelog [skip e2e] 2024-03-12 19:34:38 +05:30
StandardNotes CI 5043b4c110 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2024-03-12 11:30:04 +00:00
Aman Harwara e1981f4eae chore: add "Clear formatting" option to text format menu and make selected options clearer by showing check if selected [skip e2e] 2024-03-12 16:51:08 +05:30
Aman Harwara 3fb6d10a7f chore: fix issue with super toolbar menu items not working on mobile [skip e2e] 2024-03-12 16:33:52 +05:30
StandardNotes CI 498cd1b27c chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2024-03-11 19:25:47 +00:00
moughxyz 6d872cd45a fix: use API v0 for Android [skip e2e] 2024-03-11 14:15:15 -05:00
StandardNotes CI 9e2d496804 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2024-03-11 10:50:23 +00:00
Karol Sójko 7febc1fac6 chore: fix calling subscription settings endpoints - skip e2e 2024-03-11 11:40:58 +01:00
StandardNotes CI 6f571936c3 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2024-03-11 10:21:38 +00:00
Karol Sójko 065f3562f4 chore: fix muting sign in emails - skip e2e (#2859)
* chore: fix muting sign in emails - skip e2e

* chore: fix api version for e2e tests
2024-03-11 11:12:23 +01:00
StandardNotes CI 41f542d1c5 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2024-03-08 05:54:51 +00:00
Aman Harwara 94840a7afb chore: trigger release [skip e2e] 2024-03-08 11:15:18 +05:30
108 changed files with 2257 additions and 985 deletions
+6 -5
View File
@@ -49,11 +49,12 @@ Questions? Find answers on our [Help page](https://standardnotes.com/help).
Our web app is compiled into a folder of static HTML, JS, and CSS files. You can serve these files behind a web server to get started:
1. Clone the repo
2. `cd packages/web`
3. `yarn build`
4. `cd dist`
5. You can then use Python to serve this folder over http: `python -m http.server 8080`
1. `git clone https://github.com/standardnotes/app.git`
2. `cd app`
3. `yarn install`
4. `yarn build:web`
5. `cd packages/web`
6. You can then use Python to serve this folder over http: `python -m http.server 8080`
You can now access the app at `http://localhost:8080`.
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.26.86](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
**Note:** Version bump only for package @standardnotes/api
## [1.26.85](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
**Note:** Version bump only for package @standardnotes/api
## [1.26.84](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/api
## [1.26.83](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api",
"version": "1.26.83",
"version": "1.26.86",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -72,6 +72,7 @@ export class AuthApiService implements AuthApiServiceInterface {
password: string
codeVerifier: string
recoveryCodes: string
hvmToken?: string
}): Promise<HttpResponse<SignInWithRecoveryCodesResponseBody>> {
if (this.operationsInProgress.get(AuthApiOperations.SignInWithRecoveryCodes)) {
throw new ApiCallError(ErrorMessage.GenericInProgress)
@@ -86,6 +87,7 @@ export class AuthApiService implements AuthApiServiceInterface {
password: dto.password,
recovery_codes: dto.recoveryCodes,
username: dto.username,
hvm_token: dto.hvmToken,
})
return response
@@ -17,5 +17,6 @@ export interface AuthApiServiceInterface {
password: string
codeVerifier: string
recoveryCodes: string
hvmToken?: string
}): Promise<HttpResponse<SignInWithRecoveryCodesResponseBody>>
}
@@ -66,6 +66,7 @@ export class UserApiService implements UserApiServiceInterface {
async register(registerDTO: {
email: string
serverPassword: string
hvmToken?: string
keyParams: RootKeyParamsInterface
ephemeral: boolean
}): Promise<HttpResponse<UserRegistrationResponseBody>> {
@@ -76,6 +77,7 @@ export class UserApiService implements UserApiServiceInterface {
[ApiEndpointParam.ApiVersion]: this.apiVersion,
password: registerDTO.serverPassword,
email: registerDTO.email,
hvm_token: registerDTO.hvmToken,
ephemeral: registerDTO.ephemeral,
...registerDTO.keyParams.getPortableValue(),
})
@@ -11,6 +11,7 @@ export interface UserApiServiceInterface {
register(registerDTO: {
email: string
serverPassword: string
hvmToken?: string
keyParams: RootKeyParamsInterface
ephemeral: boolean
}): Promise<HttpResponse<UserRegistrationResponseBody>>
+28 -15
View File
@@ -1,6 +1,6 @@
import { LoggerInterface, joinPaths, sleep } from '@standardnotes/utils'
import { Environment } from '@standardnotes/models'
import { LegacySession, Session, SessionToken } from '@standardnotes/domain-core'
import { LegacySession, Result, Session, SessionToken } from '@standardnotes/domain-core'
import {
HttpStatusCode,
HttpRequestParams,
@@ -23,10 +23,11 @@ import { HttpRequestOptions } from './HttpRequestOptions'
export class HttpService implements HttpServiceInterface {
private session?: Session | LegacySession
private __latencySimulatorMs?: number
private __simulateNextSessionRefreshResponseDrop = false
private declare host: string
loggingEnabled = false
private inProgressRefreshSessionPromise?: Promise<boolean>
private inProgressRefreshSessionPromise?: Promise<Result<HttpResponse<SessionRefreshResponseBody>>>
private updateMetaCallback!: (meta: HttpResponseMeta) => void
private refreshSessionCallback!: (session: Session) => void
@@ -173,12 +174,19 @@ export class HttpService implements HttpServiceInterface {
if (this.inProgressRefreshSessionPromise) {
await this.inProgressRefreshSessionPromise
} else {
this.inProgressRefreshSessionPromise = this.refreshSession()
const isSessionRefreshed = await this.inProgressRefreshSessionPromise
this.inProgressRefreshSessionPromise = undefined
const hasSessionTokenRenewedInBetweenOurRequest = httpRequest.authentication !== this.getSessionAccessToken()
if (!hasSessionTokenRenewedInBetweenOurRequest) {
this.inProgressRefreshSessionPromise = this.refreshSession()
const isSessionRefreshedResultOrError = await this.inProgressRefreshSessionPromise
let isSessionRefreshed = false
if (!isSessionRefreshedResultOrError.isFailed()) {
isSessionRefreshed = !isErrorResponse(isSessionRefreshedResultOrError.getValue())
}
this.inProgressRefreshSessionPromise = undefined
if (!isSessionRefreshed) {
return response
if (!isSessionRefreshed) {
return response
}
}
}
@@ -190,13 +198,13 @@ export class HttpService implements HttpServiceInterface {
return response
}
async refreshSession(): Promise<boolean> {
async refreshSession(): Promise<Result<HttpResponse<SessionRefreshResponseBody>>> {
if (!this.session) {
return false
return Result.fail('No session to refresh')
}
if (this.session instanceof LegacySession) {
return false
return Result.fail('Cannot refresh legacy session')
}
const response = await this.post<SessionRefreshResponseBody>(Paths.v1.refreshSession, {
@@ -204,8 +212,13 @@ export class HttpService implements HttpServiceInterface {
refresh_token: this.session.refreshToken.value,
})
if (this.__simulateNextSessionRefreshResponseDrop) {
this.__simulateNextSessionRefreshResponseDrop = false
return Result.fail('Simulating a dropped response')
}
if (isErrorResponse(response)) {
return false
return Result.ok(response)
}
if (response.meta) {
@@ -217,7 +230,7 @@ export class HttpService implements HttpServiceInterface {
response.data.session.access_expiration,
)
if (accessTokenOrError.isFailed()) {
return false
return Result.fail(accessTokenOrError.getError())
}
const accessToken = accessTokenOrError.getValue()
@@ -227,21 +240,21 @@ export class HttpService implements HttpServiceInterface {
response.data.session.refresh_expiration,
)
if (refreshTokenOrError.isFailed()) {
return false
return Result.fail(refreshTokenOrError.getError())
}
const refreshToken = refreshTokenOrError.getValue()
const sessionOrError = Session.create(accessToken, refreshToken, response.data.session.readonly_access)
if (sessionOrError.isFailed()) {
return false
return Result.fail(sessionOrError.getError())
}
this.setSession(sessionOrError.getValue())
this.refreshSessionCallback(this.session)
return true
return Result.ok(response)
}
private params(inParams: Record<string | number | symbol, unknown>): HttpRequestParams {
@@ -1,7 +1,8 @@
import { LegacySession, Session } from '@standardnotes/domain-core'
import { LegacySession, Result, Session } from '@standardnotes/domain-core'
import { HttpRequest, HttpRequestParams, HttpResponse, HttpResponseMeta } from '@standardnotes/responses'
import { HttpRequestOptions } from './HttpRequestOptions'
import { SessionRefreshResponseBody } from '../Response'
export interface HttpServiceInterface {
setHost(host: string): void
@@ -16,7 +17,7 @@ export interface HttpServiceInterface {
runHttp<T>(httpRequest: HttpRequest): Promise<HttpResponse<T>>
setSession(session: Session | LegacySession): void
refreshSession(): Promise<boolean>
refreshSession(): Promise<Result<HttpResponse<SessionRefreshResponseBody>>>
setCallbacks(
updateMetaCallback: (meta: HttpResponseMeta) => void,
refreshSessionCallback: (session: Session) => void,
@@ -4,4 +4,5 @@ export interface SignInWithRecoveryCodesRequestParams {
password: string
code_verifier: string
recovery_codes: string
hvm_token?: string
}
@@ -6,5 +6,6 @@ export type UserRegistrationRequestParams = AnyKeyParamsContent & {
[additionalParam: string]: unknown
password: string
email: string
hvm_token?: string
ephemeral: boolean
}
+80
View File
@@ -3,6 +3,86 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.449](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.448](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.447](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-04)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.446](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-01)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.445](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-28)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.444](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-27)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.443](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.442](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.441](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.440](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.439](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.438](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.437](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.436](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.435](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.434](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-12)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.433](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.432](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.431](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.430](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-08)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.429](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)
**Note:** Version bump only for package @standardnotes/clipper
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/clipper",
"description": "Web clipper browser extension for Standard Notes",
"version": "1.1.429",
"version": "1.1.449",
"private": true,
"scripts": {
"build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js",
+80
View File
@@ -3,6 +3,86 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.110.51](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.50](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.49](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-04)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.48](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-01)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.47](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-28)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.46](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-27)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.45](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.44](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.43](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.42](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.41](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.40](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.39](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.38](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.37](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.36](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-12)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.35](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.34](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.33](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.32](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-08)
**Note:** Version bump only for package @standardnotes/desktop
## [3.110.31](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)
**Note:** Version bump only for package @standardnotes/desktop
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/desktop",
"main": "./app/dist/index.js",
"version": "3.110.31",
"version": "3.110.51",
"license": "AGPL-3.0",
"author": "Standard Notes.",
"private": true,
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.21.104](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
**Note:** Version bump only for package @standardnotes/encryption
## [1.21.103](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
**Note:** Version bump only for package @standardnotes/encryption
## [1.21.102](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-02-17)
**Note:** Version bump only for package @standardnotes/encryption
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/encryption",
"version": "1.21.102",
"version": "1.21.104",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.61.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
**Note:** Version bump only for package @standardnotes/features
## [1.61.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-11-30)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/features",
"version": "1.61.1",
"version": "1.61.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -28,6 +28,7 @@ export class NativeFeatureIdentifier extends ValueObject<NativeFeatureIdentifier
MidnightTheme: 'org.standardnotes.theme-midnight',
SolarizedDarkTheme: 'org.standardnotes.theme-solarized-dark',
TitaniumTheme: 'org.standardnotes.theme-titanium',
ProtonTheme: 'com.standardnotes.theme-proton',
PlainEditor: 'com.standardnotes.plain-text',
SuperEditor: 'com.standardnotes.super-editor',
+14 -1
View File
@@ -97,5 +97,18 @@ export function themes(): ThemeFeatureDescription[] {
no_mobile: true,
})
return [midnight, futura, solarizedDark, autobiography, dark, titanium, dynamic]
const proton: ThemeFeatureDescription = FillThemeComponentDefaults({
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
name: 'Proton',
identifier: NativeFeatureIdentifier.TYPES.ProtonTheme,
permission_name: PermissionName.ProtonTheme,
dock_icon: {
type: 'circle',
background_color: '#16141c',
foreground_color: '#ffffff',
border_color: '#4a4658',
},
})
return [midnight, futura, solarizedDark, autobiography, dark, proton, titanium, dynamic]
}
@@ -40,4 +40,5 @@ export enum PermissionName {
Clipper = 'app:clipper',
Vaults = 'app:vaults',
SharedVaults = 'server:shared-vaults',
ProtonTheme = 'theme:proton',
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.28.119](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.28.118](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.28.117](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-02-17)
**Note:** Version bump only for package @standardnotes/filepicker
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/filepicker",
"version": "1.28.117",
"version": "1.28.119",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.20.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
**Note:** Version bump only for package @standardnotes/files
## [1.20.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
**Note:** Version bump only for package @standardnotes/files
## [1.20.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-02-17)
**Note:** Version bump only for package @standardnotes/files
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files",
"version": "1.20.2",
"version": "1.20.4",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+82
View File
@@ -3,6 +3,88 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.58.108](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.107](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.106](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-04)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.105](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-01)
### Bug Fixes
* Fixed super note "Table of contents" not working on mobile ([73eba38](https://github.com/standardnotes/app/commit/73eba38c1444e85839d771fb297e3035eabd99c8))
## [3.58.104](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-28)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.103](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-27)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.102](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.101](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.100](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.99](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.98](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.97](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.96](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.95](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.94](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.93](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-12)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.92](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.91](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.90](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.89](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-08)
**Note:** Version bump only for package @standardnotes/mobile
## [3.58.88](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)
### Bug Fixes
+1 -1
View File
@@ -158,7 +158,7 @@ platform :android do
desc 'Deploy production app'
lane :prod do
version = 3_002_000 + ENV['BUILD_NUMBER'].to_i
version = 3_004_000 + ENV['BUILD_NUMBER'].to_i
deploy_android 'prod', version
end
end
+2 -2
View File
@@ -736,7 +736,7 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
boost: a7c83b31436843459a1961bfd74b96033dc77234
boost: 57d2868c099736d80fcd648bf211b4431e51a558
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: 71803c074f6325f10b5ec891c443b6bbabef0ca7
@@ -756,7 +756,7 @@ SPEC CHECKSUMS:
MMKV: 9c6c3fa4ddd849f28c7b9a5c9d23aab84f14ee35
MMKVCore: 9bb7440b170181ac5b81f542ac258103542e693d
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCTRequired: df81ab637d35fac9e6eb94611cfd20f0feb05455
RCTTypeSafety: 4636e4a36c7c2df332bda6d59b19b41c443d4287
React: e0cc5197a804031a6c53fb38483c3485fcb9d6f3
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/mobile",
"version": "3.58.88",
"version": "3.58.108",
"author": "Standard Notes.",
"private": true,
"license": "AGPL-3.0",
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.55.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
**Note:** Version bump only for package @standardnotes/models
## [1.55.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
**Note:** Version bump only for package @standardnotes/models
# [1.55.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-02-17)
### Features
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/models",
"version": "1.55.0",
"version": "1.55.2",
"publishConfig": {
"access": "public"
},
+80
View File
@@ -3,6 +3,86 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.744](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.743](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.742](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-04)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.741](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-01)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.740](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-28)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.739](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-27)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.738](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.737](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.736](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.735](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.734](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.733](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.732](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.731](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.730](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.729](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-12)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.728](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.727](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.726](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.725](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-08)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.724](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)
**Note:** Version bump only for package @standardnotes/releases
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/releases",
"version": "1.4.724",
"version": "1.4.744",
"license": "AGPL-3.0",
"main": "dist/releases.json",
"types": "dist/index.d.ts",
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.13.55](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
**Note:** Version bump only for package @standardnotes/responses
## [1.13.54](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
**Note:** Version bump only for package @standardnotes/responses
## [1.13.53](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-11-30)
**Note:** Version bump only for package @standardnotes/responses
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/responses",
"version": "1.13.53",
"version": "1.13.55",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -0,0 +1,3 @@
export type MetaEndpointResponse = {
captchaUIUrl: string | null
}
@@ -26,6 +26,30 @@ export function isErrorResponse<T>(response: HttpResponse<T>): response is HttpE
return (response.data as HttpErrorResponseBody)?.error != undefined || response.status >= 400
}
export function getCaptchaHeader<T>(response: HttpResponse<T>) {
const captchaHeader = response.headers?.get('x-captcha-required')
if (captchaHeader) {
return captchaHeader
}
return null
}
export function getErrorMessageFromErrorResponseBody(data: HttpErrorResponseBody, defaultMessage?: string): string {
let errorMessage = defaultMessage || 'Unknown error'
if (
data &&
typeof data === 'object' &&
'error' in data &&
data.error &&
typeof data.error === 'object' &&
'message' in data.error
) {
errorMessage = data.error.message as string
}
return errorMessage
}
export function getErrorFromErrorResponse(response: HttpErrorResponse): HttpError {
const embeddedError = response.data.error
if (embeddedError) {
+1
View File
@@ -13,6 +13,7 @@ export * from './Auth/SignInData'
export * from './Auth/SignInResponse'
export * from './Auth/SignOutResponse'
export * from './Auth/User'
export * from './Auth/MetaEndpointResponse'
/** Temps are awaiting final publish state on server repo */
export * from './Temp/SharedVaultMoveType'
+18
View File
@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.70.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
**Note:** Version bump only for package @standardnotes/services
## [1.70.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
**Note:** Version bump only for package @standardnotes/services
## [1.70.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
### Bug Fixes
* Fixed issue with system color scheme not being applied correctly on start ([7967dde](https://github.com/standardnotes/app/commit/7967ddead790d89478554cd6a96429fd06958590))
## [1.70.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/services
## [1.70.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/services",
"version": "1.70.3",
"version": "1.70.7",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -13,8 +13,10 @@ export interface AuthClientInterface {
password: string
codeVerifier: string
recoveryCodes: string
hvmToken?: string
}): Promise<
| {
success: true
keyParams: AnyKeyParamsContent
session: SessionBody
user: {
@@ -23,6 +25,9 @@ export interface AuthClientInterface {
protocolVersion: string
}
}
| false
| {
success: false
captchaURL: string
}
>
}
@@ -1,6 +1,6 @@
import { AuthApiServiceInterface } from '@standardnotes/api'
import { AnyKeyParamsContent } from '@standardnotes/common'
import { isErrorResponse, SessionBody } from '@standardnotes/responses'
import { isErrorResponse, getCaptchaHeader } from '@standardnotes/responses'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { AbstractService } from '../Service/AbstractService'
@@ -45,37 +45,39 @@ export class AuthManager extends AbstractService implements AuthClientInterface
}
}
async signInWithRecoveryCodes(dto: {
username: string
password: string
codeVerifier: string
recoveryCodes: string
}): Promise<
| {
keyParams: AnyKeyParamsContent
session: SessionBody
user: {
uuid: string
email: string
protocolVersion: string
}
}
| false
> {
async signInWithRecoveryCodes(
dto: Parameters<AuthClientInterface['signInWithRecoveryCodes']>[0],
): ReturnType<AuthClientInterface['signInWithRecoveryCodes']> {
try {
const result = await this.authApiService.signInWithRecoveryCodes(dto)
const captchaURL = getCaptchaHeader(result)
if (captchaURL) {
return {
success: false,
captchaURL,
}
}
if (isErrorResponse(result)) {
return false
return {
success: false,
captchaURL: '',
}
}
return {
success: true,
keyParams: result.data.key_params as AnyKeyParamsContent,
session: result.data.session,
user: result.data.user,
}
} catch (error) {
return false
return {
success: false,
captchaURL: '',
}
}
}
}
@@ -36,7 +36,8 @@ export interface ComponentManagerInterface {
getDefaultEditorIdentifier(currentTag?: SNTag): string
isThemeActive(theme: UIFeature<ThemeFeatureDescription>): boolean
toggleTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void>
toggleTheme(theme: UIFeature<ThemeFeatureDescription>, skipEntitlementCheck?: boolean): Promise<void>
toggleOtherNonLayerableThemes(uiFeature: UIFeature<ThemeFeatureDescription>): void
getActiveThemes(): UIFeature<ThemeFeatureDescription>[]
getActiveThemesIdentifiers(): { features: NativeFeatureIdentifier[]; uuids: Uuid[] }
@@ -30,13 +30,14 @@ export interface SessionsClientInterface {
revokeAllOtherSessions(): Promise<void>
isCurrentSessionReadOnly(): boolean | undefined
register(email: string, password: string, ephemeral: boolean): Promise<UserRegistrationResponseBody>
register(email: string, password: string, hvmToken: string, ephemeral: boolean): Promise<UserRegistrationResponseBody>
signIn(
email: string,
password: string,
strict: boolean,
ephemeral: boolean,
minAllowedVersion?: ProtocolVersion,
hvmToken?: string,
): Promise<SessionManagerResponse>
bypassChecksAndSignInWithRootKey(
email: string,
@@ -142,6 +142,7 @@ export class UserService
public async register(
email: string,
password: string,
hvmToken: string,
ephemeral = false,
mergeLocal = true,
): Promise<UserRegistrationResponseBody> {
@@ -157,7 +158,7 @@ export class UserService
try {
this.lockSyncing()
const response = await this.sessions.register(email, password, ephemeral)
const response = await this.sessions.register(email, password, hvmToken, ephemeral)
await this.notifyEventSync(AccountEvent.SignedInOrRegistered, {
payload: {
@@ -190,6 +191,7 @@ export class UserService
ephemeral = false,
mergeLocal = true,
awaitSync = false,
hvmToken?: string,
): Promise<HttpResponse<SignInResponse>> {
if (this.encryption.hasAccount()) {
throw Error('Tried to sign in when an account already exists.')
@@ -205,7 +207,7 @@ export class UserService
/** Prevent a timed sync from occuring while signing in. */
this.lockSyncing()
const { response } = await this.sessions.signIn(email, password, strict, ephemeral)
const { response } = await this.sessions.signIn(email, password, strict, ephemeral, undefined, hvmToken)
if (!isErrorResponse(response)) {
const notifyingFunction = awaitSync ? this.notifyEventSync.bind(this) : this.notifyEvent.bind(this)
@@ -21,6 +21,7 @@ export interface UserServiceInterface extends AbstractService<AccountEvent, Acco
register(
email: string,
password: string,
hvmToken: string,
ephemeral: boolean,
mergeLocal: boolean,
): Promise<UserRegistrationResponseBody>
@@ -31,6 +32,7 @@ export interface UserServiceInterface extends AbstractService<AccountEvent, Acco
ephemeral: boolean,
mergeLocal: boolean,
awaitSync: boolean,
hvmToken?: string,
): Promise<HttpResponse<SignInResponse>>
deleteAccount(): Promise<{
error: boolean
-1
View File
@@ -203,7 +203,6 @@ export * from './User/SignedInOrRegisteredEventPayload'
export * from './User/SignedOutEventPayload'
export * from './User/UserService'
export * from './User/UserServiceInterface'
export * from './User/UserServiceInterface'
export * from './UserEvent/NotificationService'
export * from './UserEvent/NotificationServiceEvent'
export * from './Vault/UseCase/AuthorizeVaultDeletion'
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.208.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
**Note:** Version bump only for package @standardnotes/snjs
## [2.208.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
**Note:** Version bump only for package @standardnotes/snjs
## [2.208.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
**Note:** Version bump only for package @standardnotes/snjs
## [2.208.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
### Bug Fixes
* Fixed issue with system color scheme not being applied correctly on start ([7967dde](https://github.com/standardnotes/app/commit/7967ddead790d89478554cd6a96429fd06958590))
## [2.208.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/snjs
## [2.208.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/snjs
## [2.208.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/snjs
## [2.208.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)
### Bug Fixes
+14 -6
View File
@@ -98,6 +98,7 @@ import {
SignInResponse,
ClientDisplayableError,
SessionListEntry,
MetaEndpointResponse,
} from '@standardnotes/responses'
import {
SyncService,
@@ -117,7 +118,7 @@ import {
LoggerInterface,
canBlockDeinit,
} from '@standardnotes/utils'
import { UuidString, ApplicationEventPayload } from '../Types'
import { UuidString } from '../Types'
import { applicationEventForSyncEvent } from '@Lib/Application/Event'
import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files'
import { ComputePrivateUsername } from '@standardnotes/encryption'
@@ -206,6 +207,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
'identifier',
'defaultHost',
'appVersion',
'apiVersion',
]
for (const optionName of requiredOptions) {
@@ -274,12 +276,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
}),
)
const syncEventCallback = async (eventName: SyncEvent) => {
const syncEventCallback = async (eventName: SyncEvent, data?: unknown) => {
const appEvent = applicationEventForSyncEvent(eventName)
if (appEvent) {
await encryptionService.onSyncEvent(eventName)
await this.notifyEvent(appEvent)
await this.notifyEvent(appEvent, data)
if (appEvent === ApplicationEvent.CompletedFullSync) {
if (!this.handledFullSyncStage) {
@@ -534,7 +536,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.addEventObserver(filteredCallback, event)
}
private async notifyEvent(event: ApplicationEvent, data?: ApplicationEventPayload) {
private async notifyEvent(event: ApplicationEvent, data?: unknown) {
if (event === ApplicationEvent.Started) {
this.onStart()
} else if (event === ApplicationEvent.Launched) {
@@ -767,10 +769,11 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
public async register(
email: string,
password: string,
hvmToken: string,
ephemeral = false,
mergeLocal = true,
): Promise<UserRegistrationResponseBody> {
return this.user.register(email, password, ephemeral, mergeLocal)
return this.user.register(email, password, hvmToken, ephemeral, mergeLocal)
}
/**
@@ -784,8 +787,13 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
ephemeral = false,
mergeLocal = true,
awaitSync = false,
hvmToken?: string,
): Promise<HttpResponse<SignInResponse>> {
return this.user.signIn(email, password, strict, ephemeral, mergeLocal, awaitSync)
return this.user.signIn(email, password, strict, ephemeral, mergeLocal, awaitSync, hvmToken)
}
public async getCaptchaUrl(): Promise<HttpResponse<MetaEndpointResponse>> {
return this.legacyApi.getCaptchaUrl()
}
public async changeEmail(
@@ -166,7 +166,9 @@ describe('SignInWithRecoveryCodes', () => {
})
it('should fail if the sign in with recovery code fails', async () => {
authManager.signInWithRecoveryCodes = jest.fn().mockReturnValue(false)
authManager.signInWithRecoveryCodes = jest.fn().mockReturnValue({
success: false,
})
const useCase = createUseCase()
const result = await useCase.execute({
@@ -68,9 +68,18 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<void> {
recoveryCodes: dto.recoveryCodes,
username: dto.username,
password: rootKey.serverPassword as string,
hvmToken: dto.hvmToken,
})
if (signInResult === false) {
if (signInResult.success === false) {
if (signInResult.captchaURL) {
return Result.fail(
JSON.stringify({
captchaURL: signInResult.captchaURL,
}),
)
}
return Result.fail('Could not sign in with recovery code')
}
@@ -2,4 +2,5 @@ export interface SignInWithRecoveryCodesDTO {
recoveryCodes: string
username: string
password: string
hvmToken?: string
}
+34 -1
View File
@@ -68,6 +68,7 @@ import {
isErrorResponse,
MoveFileResponse,
ValetTokenOperation,
MetaEndpointResponse,
} from '@standardnotes/responses'
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
import { HttpServiceInterface } from '@standardnotes/api'
@@ -290,6 +291,7 @@ export class LegacyApiService
email: string
serverPassword: string
ephemeral: boolean
hvmToken?: string
}): Promise<HttpResponse<SignInResponse>> {
if (this.authenticating) {
return this.createErrorResponse(API_MESSAGE_LOGIN_IN_PROGRESS, HttpStatusCode.BadRequest)
@@ -301,6 +303,7 @@ export class LegacyApiService
password: dto.serverPassword,
ephemeral: dto.ephemeral,
code_verifier: this.inMemoryStore.getValue(StorageKey.CodeVerifier) as string,
hvm_token: dto.hvmToken,
})
const response = await this.request<SignInResponse>({
@@ -408,7 +411,12 @@ export class LegacyApiService
}
}
async refreshSession(): Promise<HttpResponse<SessionRenewalResponse>> {
/**
* @deprecated
*
* This function should be replaced with @standardnotes/api's `HttpService::refreshSession` function.
*/
async deprecatedRefreshSessionOnlyUsedInE2eTests(): Promise<HttpResponse<SessionRenewalResponse>> {
const preprocessingError = this.preprocessingError()
if (preprocessingError) {
return preprocessingError
@@ -588,6 +596,26 @@ export class LegacyApiService
})
}
async updateSubscriptionSetting(
userUuid: UuidString,
settingName: string,
settingValue: string | null,
sensitive: boolean,
): Promise<HttpResponse<UpdateSettingResponse>> {
const params = {
name: settingName,
value: settingValue,
sensitive: sensitive,
}
return this.tokenRefreshableRequest<UpdateSettingResponse>({
verb: HttpVerb.Put,
url: joinPaths(this.host, Paths.v1.subscriptionSettings(userUuid)),
authentication: this.getSessionAccessToken(),
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
params,
})
}
async deleteSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<DeleteSettingResponse>> {
return this.tokenRefreshableRequest<DeleteSettingResponse>({
verb: HttpVerb.Delete,
@@ -933,4 +961,9 @@ export class LegacyApiService
return this.session.accessToken
}
public getCaptchaUrl() {
const response = this.httpService.get<MetaEndpointResponse>(Paths.v1.meta)
return response
}
}
+2
View File
@@ -42,6 +42,7 @@ const SettingsPaths = {
setting: (userUuid: string, settingName: string) => `/v1/users/${userUuid}/settings/${settingName}`,
subscriptionSetting: (userUuid: string, settingName: string) =>
`/v1/users/${userUuid}/subscription-settings/${settingName}`,
subscriptionSettings: (userUuid: string) => `/v1/users/${userUuid}/subscription-settings`,
}
const SubscriptionPaths = {
@@ -69,6 +70,7 @@ export const Paths = {
...SettingsPaths,
...SubscriptionPaths,
...UserPaths,
meta: '/v1/meta',
},
v2: {
...UserPathsV2,
@@ -390,7 +390,20 @@ export class ComponentManager
return this.viewers.find((viewer) => viewer.sessionKey === key)
}
public async toggleTheme(uiFeature: UIFeature<ThemeFeatureDescription>): Promise<void> {
public toggleOtherNonLayerableThemes(uiFeature: UIFeature<ThemeFeatureDescription>): void {
const activeThemes = this.getActiveThemes()
for (const candidate of activeThemes) {
if (candidate.featureIdentifier === uiFeature.featureIdentifier) {
continue
}
if (!candidate.layerable) {
this.removeActiveTheme(candidate)
}
}
}
public async toggleTheme(uiFeature: UIFeature<ThemeFeatureDescription>, skipEntitlementCheck = false): Promise<void> {
this.logger.info('Toggling theme', uiFeature.uniqueIdentifier)
if (this.isThemeActive(uiFeature)) {
@@ -399,7 +412,7 @@ export class ComponentManager
}
const featureStatus = this.features.getFeatureStatus(uiFeature.uniqueIdentifier)
if (featureStatus !== FeatureStatus.Entitled) {
if (!skipEntitlementCheck && featureStatus !== FeatureStatus.Entitled) {
return
}
@@ -410,16 +423,7 @@ export class ComponentManager
if (!uiFeature.layerable) {
await sleep(10)
const activeThemes = this.getActiveThemes()
for (const candidate of activeThemes) {
if (candidate.featureIdentifier === uiFeature.featureIdentifier) {
continue
}
if (!candidate.layerable) {
this.removeActiveTheme(candidate)
}
}
this.toggleOtherNonLayerableThemes(uiFeature)
}
}
@@ -454,7 +458,7 @@ export class ComponentManager
const features: NativeFeatureIdentifier[] = []
const uuids: Uuid[] = []
const strings = this.preferences.getLocalValue(LocalPrefKey.ActiveThemes, [])
const strings = new Set(this.preferences.getLocalValue(LocalPrefKey.ActiveThemes, []))
for (const string of strings) {
const nativeIdentifier = NativeFeatureIdentifier.create(string)
if (!nativeIdentifier.isFailed()) {
@@ -404,7 +404,12 @@ export class SessionManager
return undefined
}
async register(email: string, password: string, ephemeral: boolean): Promise<UserRegistrationResponseBody> {
async register(
email: string,
password: string,
hvmToken: string,
ephemeral: boolean,
): Promise<UserRegistrationResponseBody> {
if (password.length < MINIMUM_PASSWORD_LENGTH) {
throw new ApiCallError(
ErrorMessage.InsufficientPasswordMessage.replace('%LENGTH%', MINIMUM_PASSWORD_LENGTH.toString()),
@@ -429,6 +434,7 @@ export class SessionManager
const registerResponse = await this.userApiService.register({
email,
serverPassword,
hvmToken,
keyParams,
ephemeral,
})
@@ -503,8 +509,9 @@ export class SessionManager
strict = false,
ephemeral = false,
minAllowedVersion?: Common.ProtocolVersion,
hvmToken?: string,
): Promise<SessionManagerResponse> {
const result = await this.performSignIn(email, password, strict, ephemeral, minAllowedVersion)
const result = await this.performSignIn(email, password, strict, ephemeral, minAllowedVersion, hvmToken)
if (
isErrorResponse(result.response) &&
getErrorFromErrorResponse(result.response).tag !== ErrorTag.ClientValidationError &&
@@ -515,7 +522,7 @@ export class SessionManager
/**
* Try signing in with trimmed + lowercase version of email
*/
return this.performSignIn(cleanedEmail, password, strict, ephemeral, minAllowedVersion)
return this.performSignIn(cleanedEmail, password, strict, ephemeral, minAllowedVersion, hvmToken)
} else {
return result
}
@@ -530,6 +537,7 @@ export class SessionManager
strict = false,
ephemeral = false,
minAllowedVersion?: Common.ProtocolVersion,
hvmToken?: string,
): Promise<SessionManagerResponse> {
const paramsResult = await this.retrieveKeyParams({
email,
@@ -593,7 +601,7 @@ export class SessionManager
}
}
const rootKey = await this.encryptionService.computeRootKey(password, keyParams)
const signInResponse = await this.bypassChecksAndSignInWithRootKey(email, rootKey, ephemeral)
const signInResponse = await this.bypassChecksAndSignInWithRootKey(email, rootKey, ephemeral, hvmToken)
return {
response: signInResponse,
@@ -604,6 +612,7 @@ export class SessionManager
email: string,
rootKey: SNRootKey,
ephemeral = false,
hvmToken?: string,
): Promise<HttpResponse<SignInResponse>> {
const { wrappingKey, canceled } = await this.challengeService.getWrappingKeyIfApplicable()
@@ -619,6 +628,7 @@ export class SessionManager
email,
serverPassword: rootKey.serverPassword as string,
ephemeral,
hvmToken,
})
if (!signInResponse.data || isErrorResponse(signInResponse)) {
@@ -874,7 +884,14 @@ export class SessionManager
const willRefreshTokenExpireSoon = refreshTokenExpiration.getTime() - Date.now() < ThirtyMinutes
if (willAccessTokenExpireSoon || willRefreshTokenExpireSoon) {
return this.httpService.refreshSession()
const refreshSessionResultOrError = await this.httpService.refreshSession()
if (refreshSessionResultOrError.isFailed()) {
return false
}
const refreshSessionResult = refreshSessionResultOrError.getValue()
return isErrorResponse(refreshSessionResult)
}
return false
@@ -38,6 +38,10 @@ export class SettingsService extends AbstractService implements SettingsClientIn
return this.provider.getSubscriptionSetting(name)
}
async updateSubscriptionSetting(name: SettingName, payload: string, sensitive = false) {
return this.provider.updateSubscriptionSetting(name, payload, sensitive)
}
async updateSetting(name: SettingName, payload: string, sensitive = false) {
return this.provider.updateSetting(name, payload, sensitive)
}
@@ -77,6 +77,13 @@ export class SettingsGateway {
return response?.data?.setting?.value ?? undefined
}
async updateSubscriptionSetting(name: SettingName, payload: string, sensitive: boolean): Promise<void> {
const response = await this.settingsApi.updateSubscriptionSetting(this.userUuid, name.value, payload, sensitive)
if (isErrorResponse(response)) {
throw new Error(getErrorFromErrorResponse(response).message)
}
}
async getDoesSensitiveSettingExist(name: SettingName): Promise<boolean> {
if (!name.isSensitive()) {
throw new Error(`Setting ${name.value} is not sensitive`)
@@ -2,7 +2,6 @@ import { SettingName } from '@standardnotes/domain-core'
import { SettingData } from '@standardnotes/responses'
import {
MuteSignInEmailsOption,
MuteFailedBackupsEmailsOption,
EmailBackupFrequency,
ListedAuthorSecretsData,
LogSessionUserAgentOption,
@@ -13,7 +12,6 @@ type SettingType =
| EmailBackupFrequency
| ListedAuthorSecretsData
| LogSessionUserAgentOption
| MuteFailedBackupsEmailsOption
| MuteSignInEmailsOption
| MuteMarketingEmailsOption
@@ -21,5 +21,12 @@ export interface SettingsServerInterface {
getSubscriptionSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<GetSettingResponse>>
updateSubscriptionSetting(
userUuid: UuidString,
settingName: string,
settingValue: string,
sensitive: boolean,
): Promise<HttpResponse<UpdateSettingResponse>>
deleteSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<DeleteSettingResponse>>
}
+1 -1
View File
@@ -22,7 +22,7 @@ export function createApplicationWithOptions({ identifier, environment, platform
defaultHost: host || Defaults.getDefaultHost(),
appVersion: Defaults.getAppVersion(),
webSocketUrl: Defaults.getDefaultWebSocketUrl(),
apiVersion: ApiVersion.V1,
apiVersion: ApiVersion.v0,
syncCallsThresholdPerMinute,
})
}
+56 -5
View File
@@ -68,7 +68,7 @@ describe('server session', function () {
password: password,
})
const response = await application.legacyApi.refreshSession()
const response = await application.legacyApi.deprecatedRefreshSessionOnlyUsedInE2eTests()
expect(response.status).to.equal(200)
expect(response.data.session.access_token).to.be.a('string')
@@ -178,7 +178,7 @@ describe('server session', function () {
expect(sessionFromStorage.refreshExpiration).to.equal(sessionFromApiService.refreshToken.expiresAt)
expect(sessionFromStorage.readonlyAccess).to.equal(sessionFromApiService.isReadOnly())
await application.legacyApi.refreshSession()
await application.legacyApi.deprecatedRefreshSessionOnlyUsedInE2eTests()
const updatedSessionFromStorage = await getSessionFromStorage(application)
const updatedSessionFromApiService = application.legacyApi.getSession()
@@ -407,7 +407,7 @@ describe('server session', function () {
await sleepUntilSessionExpires(application, false)
const refreshSessionResponse = await application.legacyApi.refreshSession()
const refreshSessionResponse = await application.legacyApi.deprecatedRefreshSessionOnlyUsedInE2eTests()
expect(refreshSessionResponse.status).to.equal(400)
/**
@@ -452,7 +452,7 @@ describe('server session', function () {
})
application.sessions.initializeFromDisk()
const refreshSessionResponse = await application.legacyApi.refreshSession()
const refreshSessionResponse = await application.legacyApi.deprecatedRefreshSessionOnlyUsedInE2eTests()
expect(refreshSessionResponse.status).to.equal(400)
expect(refreshSessionResponse.data.error.tag).to.equal('invalid-refresh-token')
@@ -470,7 +470,7 @@ describe('server session', function () {
password: password,
})
const refreshPromise = application.legacyApi.refreshSession()
const refreshPromise = application.legacyApi.deprecatedRefreshSessionOnlyUsedInE2eTests()
const syncResponse = await application.legacyApi.sync([])
expect(syncResponse.data.error).to.be.ok
@@ -481,6 +481,57 @@ describe('server session', function () {
await refreshPromise
})
it('should tell the client to refresh the token if one is used during the cooldown period after a refresh', async function () {
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
const mimickApplyingSessionFromTheServerUnsuccessfully = () => {}
const originalSetSessionFn = application.http.setSession
const originalRefreshSessionCallbackFn = application.http.refreshSessionCallback
application.http.setSession = mimickApplyingSessionFromTheServerUnsuccessfully
application.http.refreshSessionCallback = mimickApplyingSessionFromTheServerUnsuccessfully
const refreshResultOrError = await application.http.refreshSession()
expect(refreshResultOrError.isFailed()).to.equal(false)
const refreshResult = refreshResultOrError.getValue()
expect(isErrorResponse(refreshResult)).to.equal(false)
const secondRefreshResultOrErrorWithNotAppliedSession = await application.http.refreshSession()
expect(secondRefreshResultOrErrorWithNotAppliedSession.isFailed()).to.equal(false)
const secondRefreshResultWithNotAppliedSession = secondRefreshResultOrErrorWithNotAppliedSession.getValue()
expect(isErrorResponse(secondRefreshResultWithNotAppliedSession)).to.equal(false)
application.http.setSession = originalSetSessionFn
application.http.refreshSessionCallback = originalRefreshSessionCallbackFn
})
it('if session renewal response is dropped, next sync with server should return a 498 and successfully renew the session', async function () {
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
await sleepUntilSessionExpires(application)
const refreshSpy = sinon.spy(application.http, 'refreshSession')
/**
* With this sync, we expect refreshSession to be called twice, once where the response is dropped,
* and the other time where the request succeeds
*/
application.http.__simulateNextSessionRefreshResponseDrop = true
await application.sync.sync(syncOptions)
await application.sync.sync(syncOptions)
expect(refreshSpy.callCount).to.equal(2)
})
it('notes should be synced as expected after refreshing a session', async function () {
await Factory.registerUserToApplication({
application: application,
+4 -4
View File
@@ -123,12 +123,12 @@ describe('settings service', function () {
true,
)
await application.settings.updateSetting(
SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue(),
MuteFailedBackupsEmailsOption.Muted,
SettingName.create(SettingName.NAMES.LogSessionUserAgent).getValue(),
LogSessionUserAgentOption.Enabled,
)
const settings = await application.settings.listSettings()
expect(settings.getSettingValue(SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue())).to.eql(
MuteFailedBackupsEmailsOption.Muted,
expect(settings.getSettingValue(SettingName.create(SettingName.NAMES.LogSessionUserAgent).getValue())).to.eql(
LogSessionUserAgentOption.Enabled,
)
expect(settings.getSettingValue(SettingName.create(SettingName.NAMES.MfaSecret).getValue())).to.not.be.ok
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.208.10",
"version": "2.208.17",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.8.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)
**Note:** Version bump only for package @standardnotes/styles
## [1.8.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-10-24)
**Note:** Version bump only for package @standardnotes/styles
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/styles",
"version": "1.8.1",
"version": "1.8.2",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"publishConfig": {
@@ -45,3 +45,10 @@
scrollbar-width: thin;
}
.windows-web,
.windows-desktop {
.md\:hover\:overflow-y-auto {
scrollbar-gutter: stable;
}
}
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.37.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.37.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
### Bug Fixes
* Fixed issue with system color scheme on mobile ([45ca99c](https://github.com/standardnotes/app/commit/45ca99c627e51b0b6b0b55e211788fa4cb0f1c70))
## [1.37.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
### Bug Fixes
* Fixed issue with system color scheme not being applied correctly on start ([7967dde](https://github.com/standardnotes/app/commit/7967ddead790d89478554cd6a96429fd06958590))
## [1.37.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.37.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.37.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)
**Note:** Version bump only for package @standardnotes/ui-services
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/ui-services",
"version": "1.37.3",
"version": "1.37.9",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+41 -10
View File
@@ -27,7 +27,9 @@ const DefaultThemeIdentifier = 'Default'
export class ThemeManager extends AbstractUIService {
private themesActiveInTheUI: ActiveThemeList
private lastUseDeviceThemeSettings = false
private lastUseDeviceThemeSettings: boolean | undefined
private lastAutoLightTheme: string | undefined
private lastAutoDarkTheme: string | undefined
constructor(
application: WebApplicationInterface,
@@ -89,7 +91,7 @@ export class ThemeManager extends AbstractUIService {
break
}
case ApplicationEvent.FeaturesAvailabilityChanged: {
this.handleFeaturesAvailabilityChanged()
this.handleFeaturesAvailabilityChanged().catch(console.error)
break
}
case ApplicationEvent.Launched: {
@@ -166,15 +168,25 @@ export class ThemeManager extends AbstractUIService {
this.toggleTranslucentUIColors()
const useDeviceThemeSettings = this.preferences.getLocalValue(LocalPrefKey.UseSystemColorScheme, false)
const useSystemColorScheme = this.preferences.getLocalValue(LocalPrefKey.UseSystemColorScheme, false)
const autoLightTheme = this.preferences.getLocalValue(LocalPrefKey.AutoLightThemeIdentifier, DefaultThemeIdentifier)
const autoDarkTheme = this.preferences.getLocalValue(
LocalPrefKey.AutoDarkThemeIdentifier,
NativeFeatureIdentifier.TYPES.DarkTheme,
)
const hasPreferenceChanged = useDeviceThemeSettings !== this.lastUseDeviceThemeSettings
const hasPreferenceChanged =
useSystemColorScheme !== this.lastUseDeviceThemeSettings ||
autoLightTheme !== this.lastAutoLightTheme ||
autoDarkTheme !== this.lastAutoDarkTheme
if (hasPreferenceChanged) {
this.lastUseDeviceThemeSettings = useDeviceThemeSettings
this.lastUseDeviceThemeSettings = useSystemColorScheme
this.lastAutoLightTheme = autoLightTheme
this.lastAutoDarkTheme = autoDarkTheme
}
if (hasPreferenceChanged && useDeviceThemeSettings) {
if (hasPreferenceChanged && useSystemColorScheme) {
let prefersDarkColorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches
if (this.application.isNativeMobileWeb()) {
@@ -185,7 +197,7 @@ export class ThemeManager extends AbstractUIService {
}
}
private handleFeaturesAvailabilityChanged(): void {
private async handleFeaturesAvailabilityChanged() {
let hasChange = false
for (const theme of this.themesActiveInTheUI.asThemes()) {
@@ -205,6 +217,15 @@ export class ThemeManager extends AbstractUIService {
}
}
const shouldSetThemeAsPerColorScheme = this.preferences.getLocalValue(LocalPrefKey.UseSystemColorScheme, false)
if (shouldSetThemeAsPerColorScheme) {
let prefersDarkColorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches
if (this.application.isNativeMobileWeb()) {
prefersDarkColorScheme = (await this.application.mobileDevice.getColorScheme()) === 'dark'
}
hasChange = this.setThemeAsPerColorScheme(prefersDarkColorScheme)
}
if (hasChange) {
void this.cacheThemeState()
}
@@ -218,7 +239,9 @@ export class ThemeManager extends AbstractUIService {
}
}
private setThemeAsPerColorScheme(prefersDarkColorScheme: boolean) {
private setThemeAsPerColorScheme(prefersDarkColorScheme: boolean): boolean {
let didChangeTheme = false
const preference = prefersDarkColorScheme
? LocalPrefKey.AutoDarkThemeIdentifier
: LocalPrefKey.AutoLightThemeIdentifier
@@ -239,6 +262,7 @@ export class ThemeManager extends AbstractUIService {
const toggleActiveTheme = () => {
if (activeTheme) {
void this.components.toggleTheme(activeTheme)
didChangeTheme = true
}
}
@@ -246,10 +270,17 @@ export class ThemeManager extends AbstractUIService {
toggleActiveTheme()
} else {
const theme = themes.find((theme) => theme.featureIdentifier === themeIdentifier)
if (theme && !this.components.isThemeActive(theme)) {
this.components.toggleTheme(theme).catch(console.error)
if (theme) {
if (!this.components.isThemeActive(theme)) {
this.components.toggleTheme(theme, true).catch(console.error)
} else {
this.components.toggleOtherNonLayerableThemes(theme)
}
didChangeTheme = true
}
}
return didChangeTheme
}
private async activateCachedThemes() {
+87 -1
View File
@@ -3,11 +3,97 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.192.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
**Note:** Version bump only for package @standardnotes/web
## [3.192.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
**Note:** Version bump only for package @standardnotes/web
## [3.192.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-04)
**Note:** Version bump only for package @standardnotes/web
## [3.192.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-01)
### Bug Fixes
* Fixed super note "Table of contents" not working on mobile ([73eba38](https://github.com/standardnotes/app/commit/73eba38c1444e85839d771fb297e3035eabd99c8))
## [3.192.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-28)
**Note:** Version bump only for package @standardnotes/web
## [3.192.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-27)
**Note:** Version bump only for package @standardnotes/web
## [3.192.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
**Note:** Version bump only for package @standardnotes/web
## [3.192.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
**Note:** Version bump only for package @standardnotes/web
## [3.192.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
**Note:** Version bump only for package @standardnotes/web
## [3.192.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
**Note:** Version bump only for package @standardnotes/web
## [3.192.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
**Note:** Version bump only for package @standardnotes/web
## [3.192.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
### Reverts
* Revert "feat: Markdown text is now parsed correctly when pasted" ([bb81c8a](https://github.com/standardnotes/app/commit/bb81c8acfc470df76a70ab74fa8d487216e2d190))
# [3.192.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/web
## [3.191.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
**Note:** Version bump only for package @standardnotes/web
## [3.191.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)
**Note:** Version bump only for package @standardnotes/web
## [3.191.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-12)
**Note:** Version bump only for package @standardnotes/web
## [3.191.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
### Bug Fixes
* Fixed issue with re-authentication prompts ([6d872cd](https://github.com/standardnotes/app/commit/6d872cd45a504c824d6fa2d3325e1ba2401f3046))
## [3.191.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/web
## [3.191.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
**Note:** Version bump only for package @standardnotes/web
## [3.191.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-08)
**Note:** Version bump only for package @standardnotes/web
## [3.191.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)
### Bug Fixes
* use API v0 on iOS ([#2858](https://github.com/standardnotes/app/issues/2858)) [skip e2e] ([3b2f761](https://github.com/standardnotes/app/commit/3b2f76166126a5b11f35b64aacefa37618622c3d))
* Fixed issue with re-authentication prompts ([#2858](https://github.com/standardnotes/app/issues/2858)) [skip e2e] ([3b2f761](https://github.com/standardnotes/app/commit/3b2f76166126a5b11f35b64aacefa37618622c3d))
## [3.191.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)
+232 -3
View File
@@ -1,16 +1,245 @@
{
"versions": [
{
"version": "3.192.14",
"title": "[3.192.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.192.13",
"title": "[3.192.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.192.10",
"title": "[3.192.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-04)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.192.9",
"title": "[3.192.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-01)",
"date": null,
"body": "### Bug Fixes\n\n* Fixed super note \"Table of contents\" not working on mobile ([73eba38](https://github.com/standardnotes/app/commit/73eba38c1444e85839d771fb297e3035eabd99c8))",
"parsed": {
"_": [
"Fixed super note \"Table of contents\" not working on mobile (73eba38)"
],
"Bug Fixes": [
"Fixed super note \"Table of contents\" not working on mobile (73eba38)"
]
}
},
{
"version": "3.192.8",
"title": "[3.192.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-28)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.192.7",
"title": "[3.192.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-27)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.192.6",
"title": "[3.192.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.192.5",
"title": "[3.192.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.192.4",
"title": "[3.192.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.192.3",
"title": "[3.192.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.192.2",
"title": "[3.192.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.192.1",
"title": "[3.192.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)",
"date": null,
"body": "### Reverts\n\n* Revert \"feat: Markdown text is now parsed correctly when pasted\" ([bb81c8a](https://github.com/standardnotes/app/commit/bb81c8acfc470df76a70ab74fa8d487216e2d190))",
"parsed": {
"_": [
"Revert \"feat: Markdown text is now parsed correctly when pasted\" (bb81c8a)"
],
"Reverts": [
"Revert \"feat: Markdown text is now parsed correctly when pasted\" (bb81c8a)"
]
}
},
{
"version": "3.192.0",
"title": "[3.192.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.191.24",
"title": "[3.191.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.191.23",
"title": "[3.191.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.191.22",
"title": "[3.191.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-12)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.191.21",
"title": "[3.191.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)",
"date": null,
"body": "### Bug Fixes\n\n* Fixed issue with re-authentication prompts ([6d872cd](https://github.com/standardnotes/app/commit/6d872cd45a504c824d6fa2d3325e1ba2401f3046))",
"parsed": {
"_": [
"Fixed issue with re-authentication prompts (6d872cd)"
],
"Bug Fixes": [
"Fixed issue with re-authentication prompts (6d872cd)"
]
}
},
{
"version": "3.191.20",
"title": "[3.191.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.191.19",
"title": "[3.191.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.191.18",
"title": "[3.191.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-08)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.191.17",
"title": "[3.191.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)",
"date": null,
"body": "### Bug Fixes\n\n* use API v0 on iOS ([#2858](https://github.com/standardnotes/app/issues/2858)) [skip e2e] ([3b2f761](https://github.com/standardnotes/app/commit/3b2f76166126a5b11f35b64aacefa37618622c3d))",
"body": "### Bug Fixes\n\n* Fixed issue with re-authentication prompts ([#2858](https://github.com/standardnotes/app/issues/2858)) [skip e2e] ([3b2f761](https://github.com/standardnotes/app/commit/3b2f76166126a5b11f35b64aacefa37618622c3d))",
"parsed": {
"_": [
"use API v0 on iOS (#2858) skip e2e] ([3b2f761)"
"Fixed issue with re-authentication prompts (#2858) skip e2e] ([3b2f761)"
],
"Bug Fixes": [
"use API v0 on iOS (#2858) skip e2e] ([3b2f761)"
"Fixed issue with re-authentication prompts (#2858) skip e2e] ([3b2f761)"
]
}
},
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/web",
"version": "3.191.17",
"version": "3.192.14",
"license": "AGPL-3.0",
"main": "dist/app.js",
"author": "Standard Notes",
@@ -110,7 +110,10 @@
"dependencies": {
"@ariakit/react": "^0.3.9",
"@lexical/headless": "0.13.1",
"@lexical/link": "0.13.1",
"@lexical/list": "0.13.1",
"@lexical/rich-text": "0.13.1",
"@lexical/utils": "0.13.1",
"@radix-ui/react-slot": "^1.0.1",
"@react-pdf/renderer": "^3.3.2",
"comlink": "^4.4.1",
@@ -0,0 +1,48 @@
:root {
--background-norm: #16141c;
--background-weak: #292733;
--background-strong: #3f3b4c;
--text-norm: #ffffff;
--text-weak: #a7a4b5;
--border-norm: #4a4658;
--border-weak: #343140;
--primary: #8a6eff;
--interaction-norm: #6d4aff;
--sn-stylekit-background-color: var(--background-norm);
--sn-stylekit-foreground-color: var(--text-norm);
--sn-stylekit-border-color: var(--border-weak);
--sn-stylekit-contrast-background-color: var(--background-weak);
--sn-stylekit-contrast-foreground-color: var(--text-norm);
--sn-stylekit-secondary-background-color: var(--background-weak);
--sn-stylekit-secondary-foreground-color: var(--text-norm);
--sn-stylekit-info-color: var(--interaction-norm);
--sn-stylekit-info-backdrop-color: #6e4aff0f;
--sn-stylekit-passive-color-0: var(--text-weak);
--sn-stylekit-passive-color-3: var(--background-strong);
--sn-stylekit-passive-color-4: #3f3b4c;
--sn-stylekit-passive-color-5: var(--background-weak);
--navigation-item-selected-background-color: var(--background-strong);
--sn-stylekit-paragraph-text-color: var(--text-weak);
}
a,
.Lexical__link,
.Lexical__textCode {
color: var(--primary);
}
.Collapsible__container,
.Lexical__tableCellHeader {
border-color: var(--border-norm);
}
.Lexical__quote {
color: var(--text-weak);
}
#blocks-editor hr.selected {
outline-color: var(--primary);
}
@@ -113,8 +113,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
defaultHost: defaultSyncServerHost,
appVersion: deviceInterface.appVersion,
webSocketUrl: webSocketUrl,
/** iOS file:// based origin does not work with production cookies */
apiVersion: platform === Platform.Ios ? ApiVersion.v0 : ApiVersion.v1,
/**
* iOS file:// based origin does not work with production cookies
*/
apiVersion: platform === Platform.Ios || platform === Platform.Android ? ApiVersion.v0 : ApiVersion.v1,
loadBatchSize:
deviceInterface.environment === Environment.Mobile ? 250 : ApplicationOptionsDefaults.loadBatchSize,
sleepBetweenBatches:
@@ -16,6 +16,8 @@ import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
import Icon from '@/Components/Icon/Icon'
import IconButton from '@/Components/Button/IconButton'
import { useApplication } from '../ApplicationProvider'
import { useCaptcha } from '@/Hooks/useCaptcha'
import { isErrorResponse } from '@standardnotes/snjs'
type Props = {
setMenuPane: (pane: AccountMenuPane) => void
@@ -33,6 +35,39 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
const [shouldMergeLocal, setShouldMergeLocal] = useState(true)
const [error, setError] = useState('')
const [hvmToken, setHVMToken] = useState('')
const [captchaURL, setCaptchaURL] = useState('')
const register = useCallback(() => {
setIsRegistering(true)
application
.register(email, password, hvmToken, isEphemeral, shouldMergeLocal)
.then(() => {
application.accountMenuController.closeAccountMenu()
application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu)
})
.catch((err) => {
console.error(err)
setError(err.message)
})
.finally(() => {
setIsRegistering(false)
})
}, [application, email, hvmToken, isEphemeral, password, shouldMergeLocal])
const captchaIframe = useCaptcha(captchaURL, (token) => {
setHVMToken(token)
setCaptchaURL('')
})
useEffect(() => {
if (!hvmToken) {
return
}
register()
}, [hvmToken, register])
const passwordInputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
@@ -51,6 +86,28 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
setShouldMergeLocal(!shouldMergeLocal)
}, [shouldMergeLocal])
const checkIfCaptchaRequiredAndRegister = useCallback(() => {
application
.getCaptchaUrl()
.then((response) => {
if (isErrorResponse(response)) {
throw new Error()
}
const { captchaUIUrl } = response.data
if (captchaUIUrl) {
setCaptchaURL(captchaUIUrl)
} else {
setCaptchaURL('')
register()
}
})
.catch((error) => {
console.error(error)
setCaptchaURL('')
register()
})
}, [application, register])
const handleConfirmFormSubmit: FormEventHandler = useCallback(
(e) => {
e.preventDefault()
@@ -60,28 +117,16 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
return
}
if (password === confirmPassword) {
setIsRegistering(true)
application
.register(email, password, isEphemeral, shouldMergeLocal)
.then(() => {
application.accountMenuController.closeAccountMenu()
application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu)
})
.catch((err) => {
console.error(err)
setError(err.message)
})
.finally(() => {
setIsRegistering(false)
})
} else {
if (password !== confirmPassword) {
setError(STRING_NON_MATCHING_PASSWORDS)
setConfirmPassword('')
passwordInputRef.current?.focus()
return
}
checkIfCaptchaRequiredAndRegister()
},
[application, confirmPassword, email, isEphemeral, password, shouldMergeLocal],
[checkIfCaptchaRequiredAndRegister, confirmPassword, password],
)
const handleKeyDown: KeyboardEventHandler = useCallback(
@@ -100,35 +145,26 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
setMenuPane(AccountMenuPane.Register)
}, [setMenuPane])
return (
const confirmPasswordForm = (
<>
<div className="mb-3 mt-1 flex items-center px-3">
<IconButton
icon="arrow-left"
title="Go back"
className="mr-2 flex p-0 text-neutral"
onClick={handleGoBack}
focusable={true}
disabled={isRegistering}
/>
<div className="text-base font-bold">Confirm password</div>
</div>
<div className="mb-3 px-3 text-sm">
Because your notes are encrypted using your password,{' '}
<span className="text-danger">Standard Notes does not have a password reset option</span>. If you forget your
password, you will permanently lose access to your data.
</div>
<form onSubmit={handleConfirmFormSubmit} className="mb-1 px-3">
<DecoratedPasswordInput
className={{ container: 'mb-2' }}
disabled={isRegistering}
left={[<Icon type="password" className="text-neutral" />]}
onChange={handlePasswordChange}
onKeyDown={handleKeyDown}
placeholder="Confirm password"
ref={passwordInputRef}
value={confirmPassword}
/>
{!isRegistering && (
<DecoratedPasswordInput
className={{ container: 'mb-2' }}
disabled={isRegistering}
left={[<Icon type="password" className="text-neutral" />]}
onChange={handlePasswordChange}
onKeyDown={handleKeyDown}
placeholder="Confirm password"
ref={passwordInputRef}
value={confirmPassword}
/>
)}
{error ? <div className="my-2 text-danger">{error}</div> : null}
<Button
primary
@@ -157,6 +193,23 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
</form>
</>
)
return (
<>
<div className="mb-3 mt-1 flex items-center px-3">
<IconButton
icon="arrow-left"
title="Go back"
className="mr-2 flex p-0 text-neutral"
onClick={handleGoBack}
focusable={true}
disabled={isRegistering}
/>
<div className="text-base font-bold">{captchaURL ? 'Human verification' : 'Confirm password'}</div>
</div>
{captchaURL ? <div className="p-[10px]">{captchaIframe}</div> : confirmPasswordForm}
</>
)
}
export default observer(ConfirmPassword)
@@ -10,8 +10,9 @@ import Icon from '@/Components/Icon/Icon'
import IconButton from '@/Components/Button/IconButton'
import AdvancedOptions from './AdvancedOptions'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { getErrorFromErrorResponse, isErrorResponse } from '@standardnotes/snjs'
import { getErrorFromErrorResponse, isErrorResponse, getCaptchaHeader } from '@standardnotes/snjs'
import { useApplication } from '../ApplicationProvider'
import { useCaptcha } from '@/Hooks/useCaptcha'
type Props = {
setMenuPane: (pane: AccountMenuPane) => void
@@ -34,6 +35,15 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
const [isRecoverySignIn, setIsRecoverySignIn] = useState(false)
const [captchaURL, setCaptchaURL] = useState('')
const [showCaptcha, setShowCaptcha] = useState(false)
const [hvmToken, setHVMToken] = useState('')
const captchaIframe = useCaptcha(captchaURL, (token) => {
setHVMToken(token)
setShowCaptcha(false)
setCaptchaURL('')
})
const emailInputRef = useRef<HTMLInputElement>(null)
const passwordInputRef = useRef<HTMLInputElement>(null)
@@ -95,8 +105,12 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
passwordInputRef?.current?.blur()
application
.signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal)
.signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal, false, hvmToken)
.then((response) => {
const captchaURL = getCaptchaHeader(response)
if (captchaURL) {
setCaptchaURL(captchaURL)
}
if (isErrorResponse(response)) {
throw new Error(getErrorFromErrorResponse(response).message)
}
@@ -106,12 +120,13 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
console.error(err)
setError(err.message ?? err.toString())
setPassword('')
setHVMToken('')
passwordInputRef?.current?.blur()
})
.finally(() => {
setIsSigningIn(false)
})
}, [application, email, isEphemeral, isStrictSignin, password, shouldMergeLocal])
}, [application, email, hvmToken, isEphemeral, isStrictSignin, password, shouldMergeLocal])
const recoverySignIn = useCallback(() => {
setIsSigningIn(true)
@@ -123,10 +138,21 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
recoveryCodes,
username: email,
password: password,
hvmToken,
})
.then((result) => {
if (result.isFailed()) {
throw new Error(result.getError())
const error = result.getError()
try {
const parsed = JSON.parse(error)
if (parsed.captchaURL) {
setCaptchaURL(parsed.captchaURL)
return
}
} catch (e) {
setCaptchaURL('')
}
throw new Error(error)
}
application.accountMenuController.closeAccountMenu()
})
@@ -134,12 +160,13 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
console.error(err)
setError(err.message ?? err.toString())
setPassword('')
setHVMToken('')
passwordInputRef?.current?.blur()
})
.finally(() => {
setIsSigningIn(false)
})
}, [application, email, password, recoveryCodes])
}, [application.accountMenuController, application.signInWithRecoveryCodes, email, hvmToken, password, recoveryCodes])
const onPrivateUsernameChange = useCallback(
(newisPrivateUsername: boolean, privateUsernameIdentifier?: string) => {
@@ -151,28 +178,37 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
[setEmail],
)
const performSignIn = useCallback(() => {
if (!email || email.length === 0) {
emailInputRef?.current?.focus()
return
}
if (!password || password.length === 0) {
passwordInputRef?.current?.focus()
return
}
if (isRecoverySignIn) {
recoverySignIn()
return
}
signIn()
}, [email, isRecoverySignIn, password, recoverySignIn, signIn])
const handleSignInFormSubmit = useCallback(
(e: React.SyntheticEvent) => {
e.preventDefault()
if (!email || email.length === 0) {
emailInputRef?.current?.focus()
if (captchaURL) {
setShowCaptcha(true)
return
}
if (!password || password.length === 0) {
passwordInputRef?.current?.focus()
return
}
if (isRecoverySignIn) {
recoverySignIn()
return
}
signIn()
performSignIn()
},
[email, password, isRecoverySignIn, signIn, recoverySignIn],
[captchaURL, performSignIn],
)
const handleKeyDown: KeyboardEventHandler = useCallback(
@@ -184,19 +220,16 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
[handleSignInFormSubmit],
)
return (
useEffect(() => {
if (!hvmToken) {
return
}
performSignIn()
}, [hvmToken, performSignIn])
const signInForm = (
<>
<div className="mb-3 mt-1 flex items-center px-3">
<IconButton
icon="arrow-left"
title="Go back"
className="mr-2 flex p-0 text-neutral"
onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
focusable={true}
disabled={isSigningIn}
/>
<div className="text-base font-bold">Sign in</div>
</div>
<div className="mb-1 px-3">
<DecoratedInput
className={{ container: `mb-2 ${error ? 'border-danger' : null}` }}
@@ -257,6 +290,23 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
/>
</>
)
return (
<>
<div className="mb-3 mt-1 flex items-center px-3">
<IconButton
icon="arrow-left"
title="Go back"
className="mr-2 flex p-0 text-neutral"
onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
focusable={true}
disabled={isSigningIn}
/>
<div className="text-base font-bold">{showCaptcha ? 'Human verification' : 'Sign in'}</div>
</div>
{showCaptcha ? <div className="p-[10px]">{captchaIframe}</div> : signInForm}
</>
)
}
export default observer(SignInPane)
@@ -1,6 +1,13 @@
import { WebApplicationGroup } from '@/Application/WebApplicationGroup'
import { getPlatformString } from '@/Utils'
import { ApplicationEvent, Challenge, removeFromArray, WebAppEvent } from '@standardnotes/snjs'
import {
ApplicationEvent,
Challenge,
getErrorMessageFromErrorResponseBody,
HttpErrorResponseBody,
removeFromArray,
WebAppEvent,
} from '@standardnotes/snjs'
import { alertDialog, isIOS, RouteType } from '@standardnotes/ui-services'
import { WebApplication } from '@/Application/WebApplication'
import Footer from '@/Components/Footer/Footer'
@@ -117,7 +124,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
onAppLaunch()
}
const removeAppObserver = application.addEventObserver(async (eventName) => {
const removeAppObserver = application.addEventObserver(async (eventName, data?: unknown) => {
if (eventName === ApplicationEvent.Started) {
onAppStart()
} else if (eventName === ApplicationEvent.Launched) {
@@ -147,6 +154,14 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
type: ToastType.Error,
message: 'Too many requests. Please try again later.',
})
} else if (eventName === ApplicationEvent.FailedSync) {
addToast({
type: ToastType.Error,
message: getErrorMessageFromErrorResponseBody(
data as HttpErrorResponseBody,
'Sync error. Please try again later.',
),
})
}
})
@@ -23,12 +23,15 @@ const QuickSettingsButton = ({ application, isMobileNavigation = false }: Props)
const toggleMenu = () => setIsOpen(!isOpen)
useEffect(() => {
const darkThemeFeature = new UIFeature(GetDarkThemeFeature())
return commandService.addCommandHandler({
command: TOGGLE_DARK_MODE_COMMAND,
category: 'General',
description: 'Toggle dark mode',
onKeyDown: () => {
void application.componentManager.toggleTheme(new UIFeature(GetDarkThemeFeature()))
void application.componentManager.toggleTheme(darkThemeFeature)
return true
},
})
}, [application, commandService])
@@ -42,6 +42,16 @@ const Email: FunctionComponent<Props> = ({ application }: Props) => {
}
}
const updateSubscriptionSetting = async (settingName: SettingName, payload: string): Promise<boolean> => {
try {
await application.settings.updateSubscriptionSetting(settingName, payload, false)
return true
} catch (e) {
application.alerts.alert(STRING_FAILED_TO_UPDATE_USER_SETTING).catch(console.error)
return false
}
}
const loadSettings = useCallback(async () => {
if (!application.sessions.getUser()) {
return
@@ -79,7 +89,7 @@ const Email: FunctionComponent<Props> = ({ application }: Props) => {
previousValue === MuteSignInEmailsOption.Muted ? MuteSignInEmailsOption.NotMuted : MuteSignInEmailsOption.Muted
setSignInEmailsMutedValue(newValue)
const updateResult = await updateSetting(
const updateResult = await updateSubscriptionSetting(
SettingName.create(SettingName.NAMES.MuteSignInEmails).getValue(),
newValue,
)
@@ -1,4 +1,4 @@
import { convertStringifiedBooleanToBoolean, isDesktopApplication } from '@/Utils'
import { isDesktopApplication } from '@/Utils'
import { STRING_FAILED_TO_UPDATE_USER_SETTING } from '@/Constants/Strings'
import { useCallback, useEffect, useState } from 'react'
import { WebApplication } from '@/Application/WebApplication'
@@ -6,9 +6,7 @@ import { observer } from 'mobx-react-lite'
import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
import Dropdown from '@/Components/Dropdown/Dropdown'
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
import Switch from '@/Components/Switch/Switch'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import { EmailBackupFrequency, MuteFailedBackupsEmailsOption, SettingName } from '@standardnotes/snjs'
import { EmailBackupFrequency, SettingName } from '@standardnotes/snjs'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import Spinner from '@/Components/Spinner/Spinner'
@@ -21,7 +19,6 @@ const EmailBackups = ({ application }: Props) => {
const [isLoading, setIsLoading] = useState(false)
const [emailFrequency, setEmailFrequency] = useState<EmailBackupFrequency>(EmailBackupFrequency.Disabled)
const [emailFrequencyOptions, setEmailFrequencyOptions] = useState<DropdownItem[]>([])
const [isFailedBackupEmailMuted, setIsFailedBackupEmailMuted] = useState(true)
const hasAccount = application.hasAccount()
const loadEmailFrequencySetting = useCallback(async () => {
@@ -38,14 +35,6 @@ const EmailBackups = ({ application }: Props) => {
EmailBackupFrequency.Disabled,
),
)
setIsFailedBackupEmailMuted(
convertStringifiedBooleanToBoolean(
userSettings.getSettingValue<MuteFailedBackupsEmailsOption>(
SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue(),
MuteFailedBackupsEmailsOption.NotMuted,
),
),
)
} catch (error) {
console.error(error)
} finally {
@@ -90,19 +79,6 @@ const EmailBackups = ({ application }: Props) => {
}
}
const toggleMuteFailedBackupEmails = async () => {
const previousValue = isFailedBackupEmailMuted
setIsFailedBackupEmailMuted(!isFailedBackupEmailMuted)
const updateResult = await updateSetting(
SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue(),
`${!isFailedBackupEmailMuted}`,
)
if (!updateResult) {
setIsFailedBackupEmailMuted(previousValue)
}
}
const handleEmailFrequencyChange = (item: string) => {
updateEmailFrequency(item as EmailBackupFrequency).catch(console.error)
}
@@ -132,18 +108,6 @@ const EmailBackups = ({ application }: Props) => {
/>
)}
</div>
<HorizontalSeparator classes="my-4" />
<Subtitle>Email preferences</Subtitle>
<div className="flex justify-between gap-2 md:items-center">
<div className="flex flex-col">
<Text>Receive a notification email if an email backup fails.</Text>
</div>
{isLoading ? (
<Spinner className="h-5 w-5 flex-shrink-0" />
) : (
<Switch onChange={toggleMuteFailedBackupEmails} checked={!isFailedBackupEmailMuted} />
)}
</div>
</div>
</PreferencesSegment>
</PreferencesGroup>
@@ -1,9 +1,7 @@
import { NativeFeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'
import { FunctionComponent, useEffect, useState } from 'react'
import { WebApplication } from '@/Application/WebApplication'
import { FunctionComponent } from 'react'
import TwoFactorAuthWrapper from './TwoFactorAuth/TwoFactorAuthWrapper'
import { MfaProps } from './TwoFactorAuth/MfaProps'
import Encryption from './Encryption'
import PasscodeLock from './PasscodeLock'
import Privacy from './Privacy'
@@ -12,14 +10,33 @@ import ErroredItems from './ErroredItems'
import PreferencesPane from '@/Components/Preferences/PreferencesComponents/PreferencesPane'
import BiometricsLock from '@/Components/Preferences/Panes/Security/BiometricsLock'
import MultitaskingPrivacy from '@/Components/Preferences/Panes/Security/MultitaskingPrivacy'
import U2FWrapper from './U2F/U2FWrapper'
import { TwoFactorAuth, is2FAEnabled as checkIf2FAIsEnabled } from './TwoFactorAuth/TwoFactorAuth'
import U2FView from './U2F/U2FView/U2FView'
import TwoFactorAuthView from './TwoFactorAuth/TwoFactorAuthView/TwoFactorAuthView'
interface SecurityProps extends MfaProps {
interface SecurityProps {
application: WebApplication
}
const Security: FunctionComponent<SecurityProps> = (props) => {
const isNativeMobileWeb = props.application.isNativeMobileWeb()
const [is2FAEnabled, setIs2FAEnabled] = useState(false)
const [canDisable2FA, setCanDisable2FA] = useState(true)
const [auth] = useState(
() =>
new TwoFactorAuth(props.application.sessions, props.application.mfa, (status) =>
setIs2FAEnabled(checkIf2FAIsEnabled(status)),
),
)
useEffect(() => {
auth.fetchStatus()
}, [auth])
const onU2FDevicesLoaded = (devices: Array<{ id: string; name: string }>) => {
setCanDisable2FA(devices.length === 0)
}
const isU2FFeatureAvailable =
props.application.features.getFeatureStatus(
@@ -31,8 +48,14 @@ const Security: FunctionComponent<SecurityProps> = (props) => {
<Encryption />
{props.application.items.invalidNonVaultedItems.length > 0 && <ErroredItems />}
<Protections application={props.application} />
<TwoFactorAuthWrapper application={props.application} />
{isU2FFeatureAvailable && <U2FWrapper application={props.application} />}
<TwoFactorAuthView auth={auth} application={props.application} canDisable2FA={canDisable2FA} />
{isU2FFeatureAvailable && (
<U2FView
application={props.application}
is2FAEnabled={is2FAEnabled}
loadAuthenticatorsCallback={onU2FDevicesLoaded}
/>
)}
{isNativeMobileWeb && <MultitaskingPrivacy application={props.application} />}
<PasscodeLock application={props.application} />
{isNativeMobileWeb && <BiometricsLock application={props.application} />}
@@ -1,5 +0,0 @@
import { WebApplication } from '@/Application/WebApplication'
export interface MfaProps {
application: WebApplication
}
@@ -19,6 +19,7 @@ export class TwoFactorAuth {
constructor(
private readonly sessions: SessionsClientInterface,
private readonly mfa: MfaServiceInterface,
private callback?: (status: TwoFactorStatus) => void,
) {
this._errorMessage = null
@@ -90,6 +91,9 @@ export class TwoFactorAuth {
action((active) => {
this._status = active ? 'two-factor-enabled' : 'two-factor-disabled'
this.setError(null)
if (this.callback) {
this.callback(this._status)
}
}),
)
.catch(
@@ -16,9 +16,10 @@ import ModalOverlay from '@/Components/Modal/ModalOverlay'
type Props = {
auth: TwoFactorAuth
application: WebApplication
canDisable2FA: boolean
}
const TwoFactorAuthView: FunctionComponent<Props> = ({ auth, application }) => {
const TwoFactorAuthView: FunctionComponent<Props> = ({ auth, application, canDisable2FA }) => {
const shouldShowActivationModal = auth.status !== 'fetching' && is2FAActivation(auth.status)
const activationModalTitle = shouldShowActivationModal
@@ -96,7 +97,7 @@ const TwoFactorAuthView: FunctionComponent<Props> = ({ auth, application }) => {
<TwoFactorTitle auth={auth} />
<TwoFactorDescription auth={auth} />
</div>
<TwoFactorSwitch auth={auth} />
<TwoFactorSwitch auth={auth} canDisable2FA={canDisable2FA} />
</div>
</PreferencesSegment>
@@ -6,9 +6,10 @@ import Spinner from '@/Components/Spinner/Spinner'
type Props = {
auth: TwoFactorAuth
canDisable2FA: boolean
}
const TwoFactorSwitch: FunctionComponent<Props> = ({ auth }) => {
const TwoFactorSwitch: FunctionComponent<Props> = ({ auth, canDisable2FA }) => {
if (!auth.isLoggedIn()) {
return null
}
@@ -17,7 +18,9 @@ const TwoFactorSwitch: FunctionComponent<Props> = ({ auth }) => {
return <Spinner className="h-4 w-4" />
}
return <Switch checked={!is2FADisabled(auth.status)} onChange={auth.toggle2FA} />
const shouldSwitchBeDisabled = auth.status === 'two-factor-enabled' && !canDisable2FA
return <Switch checked={!is2FADisabled(auth.status)} onChange={auth.toggle2FA} disabled={shouldSwitchBeDisabled} />
}
export default observer(TwoFactorSwitch)
@@ -1,12 +0,0 @@
import { FunctionComponent, useState } from 'react'
import { MfaProps } from './MfaProps'
import { TwoFactorAuth } from './TwoFactorAuth'
import TwoFactorAuthView from './TwoFactorAuthView/TwoFactorAuthView'
const TwoFactorAuthWrapper: FunctionComponent<MfaProps> = (props) => {
const [auth] = useState(() => new TwoFactorAuth(props.application.sessions, props.application.mfa))
auth.fetchStatus()
return <TwoFactorAuthView auth={auth} application={props.application} />
}
export default TwoFactorAuthWrapper
@@ -1,5 +0,0 @@
import { WebApplication } from '@/Application/WebApplication'
export interface U2FProps {
application: WebApplication
}
@@ -4,7 +4,11 @@ import { observer } from 'mobx-react-lite'
import { Text } from '@/Components/Preferences/PreferencesComponents/Content'
import { useApplication } from '@/Components/ApplicationProvider'
const U2FDescription: FunctionComponent = () => {
type Props = {
is2FAEnabled: boolean
}
const U2FDescription: FunctionComponent<Props> = ({ is2FAEnabled }) => {
const application = useApplication()
if (application.sessions.getUser() === undefined) {
@@ -17,6 +21,9 @@ const U2FDescription: FunctionComponent = () => {
{!application.isFullU2FClient && (
<Text className="italic">Please visit the web app in order to add a hardware security key.</Text>
)}
{!is2FAEnabled && (
<Text className="italic">You must enable two-factor authentication before adding a hardware security key.</Text>
)}
</div>
)
}
@@ -15,9 +15,11 @@ import RecoveryCodeBanner from '@/Components/RecoveryCodeBanner/RecoveryCodeBann
type Props = {
application: WebApplication
is2FAEnabled: boolean
loadAuthenticatorsCallback: (devices: Array<{ id: string; name: string }>) => void
}
const U2FView: FunctionComponent<Props> = ({ application }) => {
const U2FView: FunctionComponent<Props> = ({ application, is2FAEnabled, loadAuthenticatorsCallback }) => {
const [showDeviceAddingModal, setShowDeviceAddingModal] = useState(false)
const [devices, setDevices] = useState<Array<{ id: string; name: string }>>([])
const [error, setError] = useState('')
@@ -34,8 +36,10 @@ const U2FView: FunctionComponent<Props> = ({ application }) => {
return
}
setDevices(authenticatorListOrError.getValue())
}, [setError, setDevices, application])
const authenticatorList = authenticatorListOrError.getValue()
setDevices(authenticatorList)
loadAuthenticatorsCallback(authenticatorList)
}, [setError, setDevices, application, loadAuthenticatorsCallback])
useEffect(() => {
loadAuthenticatorDevices().catch(console.error)
@@ -47,7 +51,7 @@ const U2FView: FunctionComponent<Props> = ({ application }) => {
<PreferencesSegment>
<div className="flex flex-col">
<U2FTitle />
<U2FDescription />
<U2FDescription is2FAEnabled={is2FAEnabled} />
</div>
</PreferencesSegment>
<PreferencesSegment classes="mt-2">
@@ -60,7 +64,7 @@ const U2FView: FunctionComponent<Props> = ({ application }) => {
/>
<Button
className="mt-1"
disabled={!application.isFullU2FClient}
disabled={!application.isFullU2FClient || !is2FAEnabled}
label="Add Device"
primary
onClick={handleAddDeviceClick}
@@ -1,10 +0,0 @@
import { FunctionComponent } from 'react'
import { U2FProps } from './U2FProps'
import U2FView from './U2FView/U2FView'
const U2FWrapper: FunctionComponent<U2FProps> = (props) => {
return <U2FView application={props.application} />
}
export default U2FWrapper
@@ -1,7 +1,6 @@
import { WebApplication } from '@/Application/WebApplication'
import { MfaProps } from './Panes/Security/TwoFactorAuth/MfaProps'
export interface PreferencesProps extends MfaProps {
export interface PreferencesProps {
application: WebApplication
closePreferences: () => void
}
@@ -2,10 +2,13 @@ import Button from '@/Components/Button/Button'
import { WebApplication } from '@/Application/WebApplication'
import { PurchaseFlowPane } from '@/Controllers/PurchaseFlow/PurchaseFlowPane'
import { observer } from 'mobx-react-lite'
import { ChangeEventHandler, FunctionComponent, useEffect, useRef, useState } from 'react'
import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import FloatingLabelInput from '@/Components/Input/FloatingLabelInput'
import { isEmailValid } from '@/Utils'
import { BlueDotIcon, CircleIcon, DiamondIcon, CreateAccountIllustration } from '@standardnotes/icons'
import { useCaptcha } from '@/Hooks/useCaptcha'
import { AccountMenuPane } from '../../AccountMenu/AccountMenuPane'
import { isErrorResponse } from '@standardnotes/snjs'
type Props = {
application: WebApplication
@@ -20,6 +23,61 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
const [isEmailInvalid, setIsEmailInvalid] = useState(false)
const [isPasswordNotMatching, setIsPasswordNotMatching] = useState(false)
const [hvmToken, setHVMToken] = useState('')
const [captchaURL, setCaptchaURL] = useState('')
const register = useCallback(() => {
setIsCreatingAccount(true)
application
.register(email, password, hvmToken)
.then(() => {
application.accountMenuController.closeAccountMenu()
application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu)
})
.catch((err) => {
console.error(err)
application.alerts.alert(err as string).catch(console.error)
})
.finally(() => {
setIsCreatingAccount(false)
})
}, [application, email, hvmToken, password])
const captchaIframe = useCaptcha(captchaURL, (token) => {
setHVMToken(token)
setCaptchaURL('')
})
useEffect(() => {
if (!hvmToken) {
return
}
register()
}, [hvmToken, register])
const checkIfCaptchaRequiredAndRegister = useCallback(() => {
application
.getCaptchaUrl()
.then((response) => {
if (isErrorResponse(response)) {
throw new Error()
}
const { captchaUIUrl } = response.data
if (captchaUIUrl) {
setCaptchaURL(captchaUIUrl)
} else {
setCaptchaURL('')
register()
}
})
.catch((error) => {
console.error(error)
setCaptchaURL('')
register()
})
}, [application, register])
const emailInputRef = useRef<HTMLInputElement>(null)
const passwordInputRef = useRef<HTMLInputElement>(null)
const confirmPasswordInputRef = useRef<HTMLInputElement>(null)
@@ -81,21 +139,52 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
return
}
setIsCreatingAccount(true)
try {
await application.register(email, password)
application.purchaseFlowController.closePurchaseFlow()
void application.purchaseFlowController.openPurchaseFlow()
} catch (err) {
console.error(err)
application.alerts.alert(err as string).catch(console.error)
} finally {
setIsCreatingAccount(false)
}
checkIfCaptchaRequiredAndRegister()
}
const CreateAccountForm = (
<form onSubmit={handleCreateAccount}>
<div className="flex flex-col">
<FloatingLabelInput
className={`min-w-auto md:min-w-90 ${isEmailInvalid ? 'mb-2' : 'mb-4'}`}
id="purchase-sign-in-email"
type="email"
label="Email"
value={email}
onChange={handleEmailChange}
ref={emailInputRef}
disabled={isCreatingAccount}
isInvalid={isEmailInvalid}
/>
{isEmailInvalid ? <div className="mb-4 text-danger">Please provide a valid email.</div> : null}
<FloatingLabelInput
className="min-w-auto mb-4 md:min-w-90"
id="purchase-create-account-password"
type="password"
label="Password"
value={password}
onChange={handlePasswordChange}
ref={passwordInputRef}
disabled={isCreatingAccount}
/>
<FloatingLabelInput
className={`min-w-auto md:min-w-90 ${isPasswordNotMatching ? 'mb-2' : 'mb-4'}`}
id="create-account-confirm"
type="password"
label="Repeat password"
value={confirmPassword}
onChange={handleConfirmPasswordChange}
ref={confirmPasswordInputRef}
disabled={isCreatingAccount}
isInvalid={isPasswordNotMatching}
/>
{isPasswordNotMatching ? (
<div className="mb-4 text-danger">Passwords don't match. Please try again.</div>
) : null}
</div>
</form>
)
return (
<div className="flex items-center">
<CircleIcon className="absolute -left-28 top-[40%] h-8 w-8" />
@@ -109,46 +198,7 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
<div className="mr-0 lg:mr-12">
<h1 className="mb-2 mt-0 text-2xl font-bold">Create your free account</h1>
<div className="mb-4 text-sm font-medium">to continue to Standard Notes.</div>
<form onSubmit={handleCreateAccount}>
<div className="flex flex-col">
<FloatingLabelInput
className={`min-w-auto md:min-w-90 ${isEmailInvalid ? 'mb-2' : 'mb-4'}`}
id="purchase-sign-in-email"
type="email"
label="Email"
value={email}
onChange={handleEmailChange}
ref={emailInputRef}
disabled={isCreatingAccount}
isInvalid={isEmailInvalid}
/>
{isEmailInvalid ? <div className="mb-4 text-danger">Please provide a valid email.</div> : null}
<FloatingLabelInput
className="min-w-auto mb-4 md:min-w-90"
id="purchase-create-account-password"
type="password"
label="Password"
value={password}
onChange={handlePasswordChange}
ref={passwordInputRef}
disabled={isCreatingAccount}
/>
<FloatingLabelInput
className={`min-w-auto md:min-w-90 ${isPasswordNotMatching ? 'mb-2' : 'mb-4'}`}
id="create-account-confirm"
type="password"
label="Repeat password"
value={confirmPassword}
onChange={handleConfirmPasswordChange}
ref={confirmPasswordInputRef}
disabled={isCreatingAccount}
isInvalid={isPasswordNotMatching}
/>
{isPasswordNotMatching ? (
<div className="mb-4 text-danger">Passwords don't match. Please try again.</div>
) : null}
</div>
</form>
{captchaURL ? captchaIframe : CreateAccountForm}
<div className="flex flex-col-reverse items-start justify-between md:flex-row md:items-center">
<div className="flex flex-col">
<button
@@ -6,7 +6,8 @@ import { ChangeEventHandler, FunctionComponent, useEffect, useRef, useState } fr
import FloatingLabelInput from '@/Components/Input/FloatingLabelInput'
import { isEmailValid } from '@/Utils'
import { BlueDotIcon, CircleIcon, DiamondIcon } from '@standardnotes/icons'
import { isErrorResponse } from '@standardnotes/snjs'
import { isErrorResponse, getCaptchaHeader } from '@standardnotes/snjs'
import { useCaptcha } from '@/Hooks/useCaptcha'
type Props = {
application: WebApplication
@@ -21,6 +22,15 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
const [otherErrorMessage, setOtherErrorMessage] = useState('')
const [captchaURL, setCaptchaURL] = useState('')
const [showCaptcha, setShowCaptcha] = useState(false)
const [hvmToken, setHVMToken] = useState('')
const captchaIframe = useCaptcha(captchaURL, (token) => {
setHVMToken(token)
setShowCaptcha(false)
setCaptchaURL('')
})
const emailInputRef = useRef<HTMLInputElement>(null)
const passwordInputRef = useRef<HTMLInputElement>(null)
@@ -65,10 +75,22 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
return
}
if (captchaURL) {
setShowCaptcha(true)
return
}
setIsSigningIn(true)
try {
const response = await application.signIn(email, password)
const response = await application.signIn(email, password, undefined, undefined, undefined, undefined, hvmToken)
const captchaURL = getCaptchaHeader(response)
if (captchaURL) {
setCaptchaURL(captchaURL)
return
} else {
setCaptchaURL('')
}
if (isErrorResponse(response)) {
throw new Error(response.data.error?.message)
} else {
@@ -78,7 +100,6 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
} catch (err) {
console.error(err)
if ((err as Error).toString().includes('Invalid email or password')) {
setIsSigningIn(false)
setIsEmailInvalid(true)
setIsPasswordInvalid(true)
setOtherErrorMessage('Invalid email or password.')
@@ -86,9 +107,51 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
} else {
application.alerts.alert(err as string).catch(console.error)
}
} finally {
setIsSigningIn(false)
}
}
const signInForm = (
<form onSubmit={handleSignIn}>
<div className="flex flex-col">
<FloatingLabelInput
className={`min-w-auto sm:min-w-90 ${isEmailInvalid && !otherErrorMessage ? 'mb-2' : 'mb-4'}`}
id="purchase-sign-in-email"
type="email"
label="Email"
value={email}
onChange={handleEmailChange}
ref={emailInputRef}
disabled={isSigningIn}
isInvalid={isEmailInvalid}
/>
{isEmailInvalid && !otherErrorMessage ? (
<div className="mb-4 text-danger">Please provide a valid email.</div>
) : null}
<FloatingLabelInput
className={`min-w-auto sm:min-w-90 ${otherErrorMessage ? 'mb-2' : 'mb-4'}`}
id="purchase-sign-in-password"
type="password"
label="Password"
value={password}
onChange={handlePasswordChange}
ref={passwordInputRef}
disabled={isSigningIn}
isInvalid={isPasswordInvalid}
/>
{otherErrorMessage ? <div className="mb-4 text-danger">{otherErrorMessage}</div> : null}
</div>
<Button
className={`${isSigningIn ? 'min-w-30' : 'min-w-24'} mb-5 py-2.5`}
primary
label={isSigningIn ? 'Signing in...' : 'Sign in'}
onClick={handleSignIn}
disabled={isSigningIn}
/>
</form>
)
return (
<div className="flex items-center">
<CircleIcon className="absolute -left-56 top-[35%] h-8 w-8" />
@@ -102,43 +165,7 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
<div>
<h1 className="mb-2 mt-0 text-2xl font-bold">Sign in</h1>
<div className="mb-4 text-sm font-medium">to continue to Standard Notes.</div>
<form onSubmit={handleSignIn}>
<div className="flex flex-col">
<FloatingLabelInput
className={`min-w-auto sm:min-w-90 ${isEmailInvalid && !otherErrorMessage ? 'mb-2' : 'mb-4'}`}
id="purchase-sign-in-email"
type="email"
label="Email"
value={email}
onChange={handleEmailChange}
ref={emailInputRef}
disabled={isSigningIn}
isInvalid={isEmailInvalid}
/>
{isEmailInvalid && !otherErrorMessage ? (
<div className="mb-4 text-danger">Please provide a valid email.</div>
) : null}
<FloatingLabelInput
className={`min-w-auto sm:min-w-90 ${otherErrorMessage ? 'mb-2' : 'mb-4'}`}
id="purchase-sign-in-password"
type="password"
label="Password"
value={password}
onChange={handlePasswordChange}
ref={passwordInputRef}
disabled={isSigningIn}
isInvalid={isPasswordInvalid}
/>
{otherErrorMessage ? <div className="mb-4 text-danger">{otherErrorMessage}</div> : null}
</div>
<Button
className={`${isSigningIn ? 'min-w-30' : 'min-w-24'} mb-5 py-2.5`}
primary
label={isSigningIn ? 'Signing in...' : 'Sign in'}
onClick={handleSignIn}
disabled={isSigningIn}
/>
</form>
{showCaptcha ? captchaIframe : signInForm}
<div className="text-sm font-medium text-passive-1">
Dont have an account yet?{' '}
<a
@@ -21,7 +21,6 @@ import { $isCollapsibleContentNode } from '../../../Plugins/CollapsiblePlugin/Co
import { $isCollapsibleTitleNode } from '../../../Plugins/CollapsiblePlugin/CollapsibleTitleNode'
import { PDFDataNode, PDFWorker } from './PDFWorker'
import { wrap } from 'comlink'
import { getBase64FromBlob } from '@/Utils'
import { PrefKey, PrefValue } from '@standardnotes/snjs'
const styles = StyleSheet.create({
@@ -421,7 +420,7 @@ const getPDFDataNodesFromLexicalNodes = (nodes: LexicalNode[]): PDFDataNode[] =>
const PDFWorkerComlink = wrap<PDFWorker>(new Worker(new URL('./PDFWorker.tsx', import.meta.url)))
/**
* @returns The PDF as a base64 string
* @returns The PDF as an object url
*/
export function $generatePDFFromNodes(editor: LexicalEditor, pageSize: PrefValue[PrefKey.SuperNoteExportPDFPageSize]) {
return new Promise<string>((resolve) => {
@@ -432,9 +431,8 @@ export function $generatePDFFromNodes(editor: LexicalEditor, pageSize: PrefValue
const pdfDataNodes = getPDFDataNodesFromLexicalNodes(nodes)
void PDFWorkerComlink.renderPDF(pdfDataNodes, pageSize).then((blob) => {
void getBase64FromBlob(blob).then((base64) => {
resolve(base64)
})
const url = URL.createObjectURL(blob)
resolve(url)
})
})
})
@@ -16,6 +16,7 @@ import {
COMMAND_PRIORITY_HIGH,
COMMAND_PRIORITY_LOW,
DRAGOVER_COMMAND,
DRAGEND_COMMAND,
DROP_COMMAND,
LexicalEditor,
LexicalNode,
@@ -32,6 +33,7 @@ const DRAGGABLE_BLOCK_MENU_LEFT_SPACE = -2
const TARGET_LINE_HALF_HEIGHT = 2
const DRAGGABLE_BLOCK_MENU_CLASSNAME = 'draggable-block-menu'
const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block'
let draggedNodeKey = ''
const TEXT_BOX_HORIZONTAL_PADDING = 24
const Downward = 1
@@ -283,6 +285,9 @@ function useDraggableBlockMenu(editor: LexicalEditor, anchorElem: HTMLElement, i
if (!isHTMLElement(target)) {
return false
}
if (!draggedNodeKey) {
return false
}
const targetBlockElem = getBlockElement(anchorElem, editor, new Point(event.pageX, pageY))
const targetLineElem = targetLineRef.current
if (targetBlockElem === null || targetLineElem === null) {
@@ -331,6 +336,12 @@ function useDraggableBlockMenu(editor: LexicalEditor, anchorElem: HTMLElement, i
return true
}
function onDragEnd(): boolean {
hideTargetLine(targetLineRef.current)
draggedNodeKey = ''
return true
}
return mergeRegister(
editor.registerCommand(
DRAGOVER_COMMAND,
@@ -339,6 +350,13 @@ function useDraggableBlockMenu(editor: LexicalEditor, anchorElem: HTMLElement, i
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
DRAGEND_COMMAND,
() => {
return onDragEnd()
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
DROP_COMMAND,
(event) => {
@@ -363,10 +381,12 @@ function useDraggableBlockMenu(editor: LexicalEditor, anchorElem: HTMLElement, i
}
})
dataTransfer.setData(DRAG_DATA_FORMAT, nodeKey)
draggedNodeKey = nodeKey
}
function onDragEnd(): void {
hideTargetLine(targetLineRef.current)
draggedNodeKey = ''
}
function onTouchStart(): void {
@@ -1,285 +0,0 @@
import { getPositionedPopoverStyles } from '@/Components/Popover/GetPositionedPopoverStyles'
import {
$getSelection,
$isRangeSelection,
COMMAND_PRIORITY_LOW,
LexicalEditor,
SELECTION_CHANGE_COMMAND,
} from 'lexical'
import { useCallback, useEffect, useRef, useState } from 'react'
import { getDOMRangeRect } from '../../Lexical/Utils/getDOMRangeRect'
import { classNames } from '@standardnotes/snjs'
import Icon from '@/Components/Icon/Icon'
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
import { TOGGLE_LINK_COMMAND } from '@lexical/link'
import { mergeRegister } from '@lexical/utils'
import { KeyboardKey } from '@standardnotes/ui-services'
import Button from '@/Components/Button/Button'
import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl'
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
import { $isLinkTextNode } from './ToolbarLinkTextEditor'
import { useElementResize } from '@/Hooks/useElementRect'
import { createPortal } from 'react-dom'
import { ElementIds } from '@/Constants/ElementIDs'
import { getAdjustedStylesForNonPortalPopover } from '@/Components/Popover/Utils/getAdjustedStylesForNonPortal'
const FloatingLinkEditor = ({
linkUrl,
linkText,
isEditMode,
setEditMode,
editor,
isAutoLink,
isLinkText,
isMobile,
}: {
linkUrl: string
linkText: string
isEditMode: boolean
setEditMode: (isEditMode: boolean) => void
editor: LexicalEditor
isLinkText: boolean
isAutoLink: boolean
isMobile: boolean
}) => {
const [editedLinkUrl, setEditedLinkUrl] = useState(() => linkUrl)
useEffect(() => {
setEditedLinkUrl(linkUrl)
}, [linkUrl])
const [editedLinkText, setEditedLinkText] = useState(() => linkText)
useEffect(() => {
setEditedLinkText(linkText)
}, [linkText])
const linkEditorRef = useRef<HTMLDivElement>(null)
const rangeRect = useRef<DOMRect>()
const updateLinkEditorPosition = useCallback(() => {
if (isMobile) {
return
}
const nativeSelection = window.getSelection()
const rootElement = editor.getRootElement()
if (nativeSelection !== null && rootElement !== null) {
if (rootElement.contains(nativeSelection.anchorNode)) {
rangeRect.current = getDOMRangeRect(nativeSelection, rootElement)
}
}
const linkEditorElement = linkEditorRef.current
if (!linkEditorElement) {
setTimeout(updateLinkEditorPosition)
return
}
if (!rootElement) {
return
}
const linkEditorRect = linkEditorElement.getBoundingClientRect()
const rootElementRect = rootElement.getBoundingClientRect()
const calculatedStyles = getPositionedPopoverStyles({
align: 'center',
side: 'top',
anchorRect: rangeRect.current,
popoverRect: linkEditorRect,
documentRect: rootElementRect,
offset: 12,
maxHeightFunction: () => 'none',
})
if (calculatedStyles) {
const adjustedStyles = getAdjustedStylesForNonPortalPopover(linkEditorElement, calculatedStyles)
Object.entries(adjustedStyles).forEach(([key, value]) => {
linkEditorElement.style.setProperty(key, value)
})
linkEditorElement.style.opacity = '1'
}
}, [editor, isMobile])
useElementResize(linkEditorRef.current, updateLinkEditorPosition)
useEffect(() => {
updateLinkEditorPosition()
return mergeRegister(
editor.registerUpdateListener(() => {
updateLinkEditorPosition()
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(_payload) => {
updateLinkEditorPosition()
return false
},
COMMAND_PRIORITY_LOW,
),
)
}, [editor, updateLinkEditorPosition])
const focusInput = useCallback((input: HTMLInputElement | null) => {
if (input) {
input.focus()
}
}, [])
const handleSubmission = () => {
if (editedLinkUrl !== '') {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(editedLinkUrl))
}
editor.update(() => {
const selection = $getSelection()
if (!$isRangeSelection(selection)) {
return
}
const node = getSelectedNode(selection)
if (!$isLinkTextNode(node, selection)) {
return
}
node.setTextContent(editedLinkText)
})
setEditMode(false)
}
useEffect(() => {
setTimeout(updateLinkEditorPosition)
}, [isEditMode, updateLinkEditorPosition])
return createPortal(
<div
id="super-link-editor"
className="absolute bottom-12 left-1/2 z-modal w-[calc(100%_-_1rem)] -translate-x-1/2 rounded-lg border border-border bg-contrast px-2 py-1 shadow-sm shadow-contrast translucent-ui:border-[--popover-border-color] translucent-ui:bg-[--popover-background-color] translucent-ui:[backdrop-filter:var(--popover-backdrop-filter)] md:bottom-[unset] md:left-0 md:top-0 md:w-auto md:translate-x-0 md:opacity-0"
ref={linkEditorRef}
>
{isEditMode ? (
<div
className="flex flex-col gap-2 py-1"
onBlur={(event) => {
if (!linkEditorRef.current?.contains(event.relatedTarget as Node)) {
setEditMode(false)
}
}}
>
{isLinkText && (
<div className="flex items-center gap-1.5">
<Icon type="plain-text" className="flex-shrink-0" />
<input
value={editedLinkText}
onChange={(event) => {
setEditedLinkText(event.target.value)
}}
onKeyDown={(event) => {
if (event.key === KeyboardKey.Enter) {
event.preventDefault()
handleSubmission()
} else if (event.key === KeyboardKey.Escape) {
event.preventDefault()
setEditMode(false)
}
}}
className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[20ch]"
/>
</div>
)}
<div className="flex items-center gap-1.5">
<Icon type="link" className="flex-shrink-0" />
<input
ref={focusInput}
value={editedLinkUrl}
onChange={(event) => {
setEditedLinkUrl(event.target.value)
}}
onKeyDown={(event) => {
if (event.key === KeyboardKey.Enter) {
event.preventDefault()
handleSubmission()
} else if (event.key === KeyboardKey.Escape) {
event.preventDefault()
setEditMode(false)
}
}}
className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[40ch]"
/>
</div>
<div className="flex items-center justify-end gap-1.5">
<StyledTooltip showOnMobile showOnHover label="Cancel editing">
<Button
onClick={() => {
setEditMode(false)
editor.focus()
}}
onMouseDown={(event) => event.preventDefault()}
>
Cancel
</Button>
</StyledTooltip>
<StyledTooltip showOnMobile showOnHover label="Save link">
<Button primary onClick={handleSubmission} onMouseDown={(event) => event.preventDefault()}>
Apply
</Button>
</StyledTooltip>
</div>
</div>
) : (
<div className="flex items-center gap-1">
<a
className={classNames(
'mr-1 flex flex-grow items-center gap-2 overflow-hidden whitespace-nowrap underline',
isAutoLink && 'py-2.5',
)}
href={linkUrl}
target="_blank"
rel="noopener noreferrer"
>
<Icon type="open-in" className="ml-1 flex-shrink-0" />
<div className="max-w-[35ch] overflow-hidden text-ellipsis">{linkUrl}</div>
</a>
<StyledTooltip showOnMobile showOnHover label="Copy link">
<button
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
onClick={() => {
navigator.clipboard.writeText(linkUrl).catch(console.error)
}}
onMouseDown={(event) => event.preventDefault()}
>
<Icon type="copy" size="medium" />
</button>
</StyledTooltip>
{!isAutoLink && (
<>
<StyledTooltip showOnMobile showOnHover label="Edit link">
<button
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
onClick={() => {
setEditedLinkUrl(linkUrl)
setEditMode(true)
}}
onMouseDown={(event) => event.preventDefault()}
>
<Icon type="pencil-filled" size="medium" />
</button>
</StyledTooltip>
<StyledTooltip showOnMobile showOnHover label="Remove link">
<button
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
onClick={() => {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
}}
onMouseDown={(event) => event.preventDefault()}
>
<Icon type="trash-filled" size="medium" />
</button>
</StyledTooltip>
</>
)}
</div>
)}
</div>,
document.getElementById(ElementIds.SuperEditor) ?? document.body,
)
}
export default FloatingLinkEditor
@@ -0,0 +1,261 @@
import { getPositionedPopoverStyles } from '@/Components/Popover/GetPositionedPopoverStyles'
import {
$isTextNode,
COMMAND_PRIORITY_LOW,
LexicalEditor,
RangeSelection,
SELECTION_CHANGE_COMMAND,
TextNode,
} from 'lexical'
import { useCallback, useEffect, useRef, useState } from 'react'
import { getDOMRangeRect } from '../../Lexical/Utils/getDOMRangeRect'
import { classNames } from '@standardnotes/snjs'
import Icon from '@/Components/Icon/Icon'
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
import { mergeRegister } from '@lexical/utils'
import { KeyboardKey } from '@standardnotes/ui-services'
import Button from '@/Components/Button/Button'
import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl'
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
import { useElementResize } from '@/Hooks/useElementRect'
import { createPortal } from 'react-dom'
import { ElementIds } from '@/Constants/ElementIDs'
import { getAdjustedStylesForNonPortalPopover } from '@/Components/Popover/Utils/getAdjustedStylesForNonPortal'
export const $isLinkTextNode = (
node: ReturnType<typeof getSelectedNode>,
selection: RangeSelection,
): node is TextNode => {
const parent = node.getParent()
return (
$isLinkNode(parent) &&
parent.getChildrenSize() === 1 &&
$isTextNode(node) &&
parent.getFirstChild() === node &&
selection.anchor.getNode() === selection.focus.getNode()
)
}
const LinkEditor = ({
editor,
setIsEditingLink,
isMobile,
linkNode,
linkTextNode,
}: {
editor: LexicalEditor
setIsEditingLink: (isEditMode: boolean) => void
isMobile: boolean
linkNode: LinkNode | null
linkTextNode: TextNode | null
}) => {
const [url, setURL] = useState('')
const [text, setText] = useState('')
useEffect(() => {
editor.getEditorState().read(() => {
if (linkNode) {
setURL(linkNode.getURL())
}
if (linkTextNode) {
setText(linkTextNode.getTextContent())
}
})
}, [editor, linkNode, linkTextNode])
const linkInputRef = useRef<HTMLInputElement>(null)
const linkEditorRef = useRef<HTMLDivElement>(null)
const rangeRect = useRef<DOMRect>()
const positionUpdateRAF = useRef<number>()
const updateLinkEditorPosition = useCallback(() => {
if (positionUpdateRAF.current) {
cancelAnimationFrame(positionUpdateRAF.current)
}
positionUpdateRAF.current = requestAnimationFrame(() => {
if (isMobile) {
linkInputRef.current?.focus()
return
}
const nativeSelection = window.getSelection()
const rootElement = editor.getRootElement()
if (nativeSelection !== null && rootElement !== null) {
if (rootElement.contains(nativeSelection.anchorNode)) {
rangeRect.current = getDOMRangeRect(nativeSelection, rootElement)
}
}
const linkEditorElement = linkEditorRef.current
if (!linkEditorElement) {
setTimeout(updateLinkEditorPosition)
return
}
if (!rootElement) {
return
}
if (!rangeRect.current) {
return
}
const linkEditorRect = linkEditorElement.getBoundingClientRect()
const rootElementRect = rootElement.getBoundingClientRect()
const calculatedStyles = getPositionedPopoverStyles({
align: 'center',
side: 'top',
anchorRect: rangeRect.current,
popoverRect: linkEditorRect,
documentRect: rootElementRect,
offset: 12,
maxHeightFunction: () => 'none',
})
if (calculatedStyles) {
const adjustedStyles = getAdjustedStylesForNonPortalPopover(linkEditorElement, calculatedStyles)
Object.entries(adjustedStyles).forEach(([key, value]) => {
linkEditorElement.style.setProperty(key, value)
})
linkEditorElement.style.display = 'block'
linkInputRef.current?.focus()
}
})
}, [editor, isMobile])
useElementResize(linkEditorRef.current, updateLinkEditorPosition)
useEffect(() => {
updateLinkEditorPosition()
return mergeRegister(
editor.registerUpdateListener(() => {
updateLinkEditorPosition()
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(_payload) => {
updateLinkEditorPosition()
return false
},
COMMAND_PRIORITY_LOW,
),
)
}, [editor, updateLinkEditorPosition])
const handleSubmission = () => {
if (url !== '') {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(url))
}
if (linkTextNode !== null && text !== '') {
editor.update(
() => {
linkTextNode.setTextContent(text)
},
{
discrete: true,
},
)
}
setIsEditingLink(false)
}
useEffect(() => {
const linkEditor = linkEditorRef.current
if (!linkEditor) {
return
}
const handleFocusOut = (event: FocusEvent) => {
if (!linkEditor.contains(event.relatedTarget as Node)) {
setIsEditingLink(false)
}
}
linkEditor.addEventListener('focusout', handleFocusOut)
return () => {
linkEditor.removeEventListener('focusout', handleFocusOut)
}
}, [setIsEditingLink])
return createPortal(
<div
className={classNames(
'absolute z-dropdown-menu rounded-lg border border-border bg-contrast px-2 py-1 shadow-sm shadow-contrast',
isMobile
? 'bottom-12 left-1/2 w-[calc(100%_-_1rem)] -translate-x-1/2'
: 'left-0 top-0 hidden w-auto translate-x-0 translucent-ui:border-[--popover-border-color] translucent-ui:bg-[--popover-background-color] translucent-ui:[backdrop-filter:var(--popover-backdrop-filter)]',
)}
ref={linkEditorRef}
>
<div className="flex flex-col gap-2 py-1">
{linkTextNode && (
<div className="flex items-center gap-1.5">
<Icon type="plain-text" className="flex-shrink-0" />
<input
value={text}
onChange={(event) => {
setText(event.target.value)
}}
onKeyDown={(event) => {
if (event.key === KeyboardKey.Enter) {
event.preventDefault()
handleSubmission()
} else if (event.key === KeyboardKey.Escape) {
event.preventDefault()
setIsEditingLink(false)
}
}}
className="flex-grow rounded-sm border border-border bg-contrast p-1 text-text sm:min-w-[20ch] translucent-ui:md:border-0"
/>
</div>
)}
<div className="flex items-center gap-1.5">
<Icon type="link" className="flex-shrink-0" />
<input
ref={linkInputRef}
value={url}
onChange={(event) => {
setURL(event.target.value)
}}
onKeyDown={(event) => {
if (event.key === KeyboardKey.Enter) {
event.preventDefault()
handleSubmission()
} else if (event.key === KeyboardKey.Escape) {
event.preventDefault()
setIsEditingLink(false)
}
}}
className="flex-grow rounded-sm border border-border bg-contrast p-1 text-text sm:min-w-[40ch] translucent-ui:md:border-0"
/>
</div>
<div className="flex items-center justify-end gap-1.5">
<StyledTooltip showOnMobile showOnHover label="Cancel editing">
<Button
onClick={() => {
setIsEditingLink(false)
editor.focus()
}}
onMouseDown={(event) => event.preventDefault()}
>
Cancel
</Button>
</StyledTooltip>
<StyledTooltip showOnMobile showOnHover label="Save link">
<Button primary onClick={handleSubmission} onMouseDown={(event) => event.preventDefault()}>
Apply
</Button>
</StyledTooltip>
</div>
</div>
</div>,
document.getElementById(ElementIds.SuperEditor) ?? document.body,
)
}
export default LinkEditor
@@ -0,0 +1,172 @@
import Icon from '@/Components/Icon/Icon'
import { getPositionedPopoverStyles } from '@/Components/Popover/GetPositionedPopoverStyles'
import { getAdjustedStylesForNonPortalPopover } from '@/Components/Popover/Utils/getAdjustedStylesForNonPortal'
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
import { useElementResize } from '@/Hooks/useElementRect'
import { $isAutoLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
import { mergeRegister } from '@lexical/utils'
import { classNames } from '@standardnotes/snjs'
import { COMMAND_PRIORITY_LOW, LexicalEditor, SELECTION_CHANGE_COMMAND } from 'lexical'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { getDOMRangeRect } from '../../Lexical/Utils/getDOMRangeRect'
import { ElementIds } from '@/Constants/ElementIDs'
import { createPortal } from 'react-dom'
type Props = {
linkNode: LinkNode
editor: LexicalEditor
isMobile: boolean
setIsEditingLink: (isEditingLink: boolean) => void
}
const LinkViewer = ({ isMobile, editor, linkNode, setIsEditingLink }: Props) => {
const linkViewerRef = useRef<HTMLDivElement>(null)
const [linkUrl, isAutoLink] = useMemo(() => {
let linkUrl = ''
let isAutoLink = false
editor.getEditorState().read(() => {
linkUrl = linkNode.getURL()
isAutoLink = $isAutoLinkNode(linkNode)
})
return [linkUrl, isAutoLink]
}, [editor, linkNode])
const rangeRect = useRef<DOMRect>()
const updateLinkEditorPosition = useCallback(() => {
if (isMobile) {
return
}
const nativeSelection = window.getSelection()
const rootElement = editor.getRootElement()
if (nativeSelection !== null && rootElement !== null) {
if (rootElement.contains(nativeSelection.anchorNode)) {
rangeRect.current = getDOMRangeRect(nativeSelection, rootElement)
}
}
const linkEditorElement = linkViewerRef.current
if (!linkEditorElement) {
setTimeout(updateLinkEditorPosition)
return
}
if (!rootElement) {
return
}
const linkEditorRect = linkEditorElement.getBoundingClientRect()
const rootElementRect = rootElement.getBoundingClientRect()
const calculatedStyles = getPositionedPopoverStyles({
align: 'center',
side: 'top',
anchorRect: rangeRect.current,
popoverRect: linkEditorRect,
documentRect: rootElementRect,
offset: 12,
maxHeightFunction: () => 'none',
})
if (calculatedStyles) {
const adjustedStyles = getAdjustedStylesForNonPortalPopover(linkEditorElement, calculatedStyles)
Object.entries(adjustedStyles).forEach(([key, value]) => {
linkEditorElement.style.setProperty(key, value)
})
linkEditorElement.style.opacity = '1'
}
}, [editor, isMobile])
useElementResize(linkViewerRef.current, updateLinkEditorPosition)
useEffect(() => {
updateLinkEditorPosition()
return mergeRegister(
editor.registerUpdateListener(() => {
updateLinkEditorPosition()
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(_payload) => {
updateLinkEditorPosition()
return false
},
COMMAND_PRIORITY_LOW,
),
)
}, [editor, updateLinkEditorPosition])
if (!linkUrl) {
return null
}
return createPortal(
<div
className={classNames(
'absolute z-dropdown-menu rounded-lg border border-border bg-contrast px-2 py-1 shadow-sm shadow-contrast',
isMobile
? 'bottom-12 left-1/2 w-[calc(100%_-_1rem)] -translate-x-1/2'
: 'left-0 top-0 w-auto translate-x-0 opacity-0 translucent-ui:border-[--popover-border-color] translucent-ui:bg-[--popover-background-color] translucent-ui:[backdrop-filter:var(--popover-backdrop-filter)]',
)}
ref={linkViewerRef}
>
<div className="flex items-center gap-1">
<a
className={classNames(
'mr-1 flex flex-grow items-center gap-2 overflow-hidden whitespace-nowrap underline',
isAutoLink && 'py-2.5',
)}
href={linkUrl}
target="_blank"
rel="noopener noreferrer"
>
<Icon type="open-in" className="ml-1 flex-shrink-0" />
<div className="max-w-[35ch] overflow-hidden text-ellipsis">{linkUrl}</div>
</a>
<StyledTooltip showOnMobile showOnHover label="Copy link">
<button
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
onClick={() => {
navigator.clipboard.writeText(linkUrl).catch(console.error)
}}
onMouseDown={(event) => event.preventDefault()}
>
<Icon type="copy" size="medium" />
</button>
</StyledTooltip>
{!isAutoLink && (
<>
<StyledTooltip showOnMobile showOnHover label="Edit link">
<button
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
onClick={() => {
setIsEditingLink(true)
}}
onMouseDown={(event) => event.preventDefault()}
>
<Icon type="pencil-filled" size="medium" />
</button>
</StyledTooltip>
<StyledTooltip showOnMobile showOnHover label="Remove link">
<button
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
onClick={() => {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
}}
onMouseDown={(event) => event.preventDefault()}
>
<Icon type="trash-filled" size="medium" />
</button>
</StyledTooltip>
</>
)}
</div>
</div>,
document.getElementById(ElementIds.SuperEditor) ?? document.body,
)
}
export default LinkViewer

Some files were not shown because too many files have changed in this diff Show More