mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-04-27 21:35:51 +00:00
Merge branch 'main' into dependabot/npm_and_yarn/frontend/graphql-codegen-97975e453b
This commit is contained in:
Generated
+7
-7
@@ -742,9 +742,9 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.8.0"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -1120,9 +1120,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.7.1"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
|
||||
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
@@ -3034,7 +3034,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6773,9 +6773,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.14.0"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1"
|
||||
checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ version = "1.6.0"
|
||||
|
||||
# Packed bitfields
|
||||
[workspace.dependencies.bitflags]
|
||||
version = "2.8.0"
|
||||
version = "2.9.0"
|
||||
|
||||
# Bytes
|
||||
[workspace.dependencies.bytes]
|
||||
|
||||
@@ -6,18 +6,75 @@
|
||||
|
||||
use anyhow::Context as _;
|
||||
use async_graphql::{Context, Description, Enum, ID, InputObject, Object};
|
||||
use mas_data_model::SiteConfig;
|
||||
use mas_i18n::DataLocale;
|
||||
use mas_storage::{
|
||||
RepositoryAccess,
|
||||
BoxRepository, RepositoryAccess,
|
||||
queue::{ProvisionUserJob, QueueJobRepositoryExt as _, SendEmailAuthenticationCodeJob},
|
||||
user::{UserEmailFilter, UserEmailRepository, UserRepository},
|
||||
};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::graphql::{
|
||||
model::{NodeType, User, UserEmail, UserEmailAuthentication},
|
||||
state::ContextExt,
|
||||
use crate::{
|
||||
graphql::{
|
||||
Requester,
|
||||
model::{NodeType, User, UserEmail, UserEmailAuthentication},
|
||||
state::ContextExt,
|
||||
},
|
||||
passwords::PasswordManager,
|
||||
};
|
||||
|
||||
/// Check the password if neeed
|
||||
///
|
||||
/// Returns true if password verification is not needed, or if the password is
|
||||
/// correct. Returns false if the password is incorrect or missing.
|
||||
async fn verify_password_if_needed(
|
||||
requester: &Requester,
|
||||
config: &SiteConfig,
|
||||
password_manager: &PasswordManager,
|
||||
password: Option<String>,
|
||||
user: &mas_data_model::User,
|
||||
repo: &mut BoxRepository,
|
||||
) -> Result<bool, async_graphql::Error> {
|
||||
// If the requester is admin, they don't need to provide a password
|
||||
if requester.is_admin() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// If password login is disabled, assume we don't want the user to reauth
|
||||
if !config.password_login_enabled {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Else we need to check if the user has a password
|
||||
let Some(user_password) = repo
|
||||
.user_password()
|
||||
.active(user)
|
||||
.await
|
||||
.context("Failed to load user password")?
|
||||
else {
|
||||
// User has no password, so we don't need to verify the password
|
||||
return Ok(true);
|
||||
};
|
||||
|
||||
let Some(password) = password else {
|
||||
// There is a password on the user, but not provided in the input
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let password = Zeroizing::new(password.into_bytes());
|
||||
|
||||
let res = password_manager
|
||||
.verify(
|
||||
user_password.version,
|
||||
password,
|
||||
user_password.hashed_password,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(res.is_ok())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UserEmailMutations {
|
||||
_private: (),
|
||||
@@ -120,6 +177,10 @@ impl AddEmailPayload {
|
||||
struct RemoveEmailInput {
|
||||
/// The ID of the email address to remove
|
||||
user_email_id: ID,
|
||||
|
||||
/// The user's current password. This is required if the user is not an
|
||||
/// admin and it has a password on its account.
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
/// The status of the `removeEmail` mutation
|
||||
@@ -130,6 +191,9 @@ enum RemoveEmailStatus {
|
||||
|
||||
/// The email address was not found
|
||||
NotFound,
|
||||
|
||||
/// The password provided is incorrect
|
||||
IncorrectPassword,
|
||||
}
|
||||
|
||||
/// The payload of the `removeEmail` mutation
|
||||
@@ -137,6 +201,7 @@ enum RemoveEmailStatus {
|
||||
enum RemoveEmailPayload {
|
||||
Removed(mas_data_model::UserEmail),
|
||||
NotFound,
|
||||
IncorrectPassword,
|
||||
}
|
||||
|
||||
#[Object(use_type_description)]
|
||||
@@ -146,6 +211,7 @@ impl RemoveEmailPayload {
|
||||
match self {
|
||||
RemoveEmailPayload::Removed(_) => RemoveEmailStatus::Removed,
|
||||
RemoveEmailPayload::NotFound => RemoveEmailStatus::NotFound,
|
||||
RemoveEmailPayload::IncorrectPassword => RemoveEmailStatus::IncorrectPassword,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,20 +219,23 @@ impl RemoveEmailPayload {
|
||||
async fn email(&self) -> Option<UserEmail> {
|
||||
match self {
|
||||
RemoveEmailPayload::Removed(email) => Some(UserEmail(email.clone())),
|
||||
RemoveEmailPayload::NotFound => None,
|
||||
RemoveEmailPayload::NotFound | RemoveEmailPayload::IncorrectPassword => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The user to whom the email address belonged
|
||||
async fn user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
|
||||
let state = ctx.state();
|
||||
let mut repo = state.repository().await?;
|
||||
|
||||
let user_id = match self {
|
||||
RemoveEmailPayload::Removed(email) => email.user_id,
|
||||
RemoveEmailPayload::NotFound => return Ok(None),
|
||||
RemoveEmailPayload::NotFound | RemoveEmailPayload::IncorrectPassword => {
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
let mut repo = state.repository().await?;
|
||||
|
||||
let user = repo
|
||||
.user()
|
||||
.lookup(user_id)
|
||||
@@ -226,6 +295,10 @@ struct StartEmailAuthenticationInput {
|
||||
/// The email address to add to the account
|
||||
email: String,
|
||||
|
||||
/// The user's current password. This is required if the user has a password
|
||||
/// on its account.
|
||||
password: Option<String>,
|
||||
|
||||
/// The language to use for the email
|
||||
#[graphql(default = "en")]
|
||||
language: String,
|
||||
@@ -244,6 +317,8 @@ enum StartEmailAuthenticationStatus {
|
||||
Denied,
|
||||
/// The email address is already in use on this account
|
||||
InUse,
|
||||
/// The password provided is incorrect
|
||||
IncorrectPassword,
|
||||
}
|
||||
|
||||
/// The payload of the `startEmailAuthentication` mutation
|
||||
@@ -256,6 +331,7 @@ enum StartEmailAuthenticationPayload {
|
||||
violations: Vec<mas_policy::Violation>,
|
||||
},
|
||||
InUse,
|
||||
IncorrectPassword,
|
||||
}
|
||||
|
||||
#[Object(use_type_description)]
|
||||
@@ -268,6 +344,7 @@ impl StartEmailAuthenticationPayload {
|
||||
Self::RateLimited => StartEmailAuthenticationStatus::RateLimited,
|
||||
Self::Denied { .. } => StartEmailAuthenticationStatus::Denied,
|
||||
Self::InUse => StartEmailAuthenticationStatus::InUse,
|
||||
Self::IncorrectPassword => StartEmailAuthenticationStatus::IncorrectPassword,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,9 +352,11 @@ impl StartEmailAuthenticationPayload {
|
||||
async fn authentication(&self) -> Option<&UserEmailAuthentication> {
|
||||
match self {
|
||||
Self::Started(authentication) => Some(authentication),
|
||||
Self::InvalidEmailAddress | Self::RateLimited | Self::Denied { .. } | Self::InUse => {
|
||||
None
|
||||
}
|
||||
Self::InvalidEmailAddress
|
||||
| Self::RateLimited
|
||||
| Self::Denied { .. }
|
||||
| Self::InUse
|
||||
| Self::IncorrectPassword => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,6 +573,20 @@ impl UserEmailMutations {
|
||||
.await?
|
||||
.context("Failed to load user")?;
|
||||
|
||||
// Validate the password input if needed
|
||||
if !verify_password_if_needed(
|
||||
requester,
|
||||
state.site_config(),
|
||||
&state.password_manager(),
|
||||
input.password,
|
||||
&user,
|
||||
&mut repo,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(RemoveEmailPayload::IncorrectPassword);
|
||||
}
|
||||
|
||||
// TODO: don't allow removing the last email address
|
||||
|
||||
repo.user_email().remove(user_email.clone()).await?;
|
||||
@@ -627,6 +720,20 @@ impl UserEmailMutations {
|
||||
});
|
||||
}
|
||||
|
||||
// Validate the password input if needed
|
||||
if !verify_password_if_needed(
|
||||
requester,
|
||||
state.site_config(),
|
||||
&state.password_manager(),
|
||||
input.password,
|
||||
&browser_session.user,
|
||||
&mut repo,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(StartEmailAuthenticationPayload::IncorrectPassword);
|
||||
}
|
||||
|
||||
// Create a new authentication session
|
||||
let authentication = repo
|
||||
.user_email()
|
||||
|
||||
@@ -15,7 +15,7 @@ workspace = true
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
camino.workspace = true
|
||||
convert_case = "0.7.1"
|
||||
convert_case = "0.8.0"
|
||||
csv = "1.3.1"
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -26,7 +26,7 @@ opentelemetry-semantic-conventions.workspace = true
|
||||
rand.workspace = true
|
||||
rand_chacha.workspace = true
|
||||
url.workspace = true
|
||||
uuid = "1.14.0"
|
||||
uuid = "1.15.1"
|
||||
ulid = { workspace = true, features = ["uuid"] }
|
||||
|
||||
oauth2-types.workspace = true
|
||||
|
||||
@@ -25,7 +25,7 @@ tracing.workspace = true
|
||||
futures-util = "0.3.31"
|
||||
|
||||
rand.workspace = true
|
||||
uuid = "1.14.0"
|
||||
uuid = "1.15.1"
|
||||
ulid = { workspace = true, features = ["uuid"] }
|
||||
|
||||
mas-config.workspace = true
|
||||
|
||||
@@ -172,14 +172,15 @@ const MAX_CONCURRENT_JOBS: usize = 10;
|
||||
const MAX_JOBS_TO_FETCH: usize = 5;
|
||||
|
||||
// How many attempts a job should be retried
|
||||
const MAX_ATTEMPTS: usize = 5;
|
||||
const MAX_ATTEMPTS: usize = 10;
|
||||
|
||||
/// Returns the delay to wait before retrying a job
|
||||
///
|
||||
/// Uses an exponential backoff: 1s, 2s, 4s, 8s, 16s
|
||||
/// Uses an exponential backoff: 5s, 10s, 20s, 40s, 1m20s, 2m40s, 5m20s, 10m50s,
|
||||
/// 21m40s, 43m20s
|
||||
fn retry_delay(attempt: usize) -> Duration {
|
||||
let attempt = u32::try_from(attempt).unwrap_or(u32::MAX);
|
||||
Duration::milliseconds(2_i64.saturating_pow(attempt) * 1000)
|
||||
Duration::milliseconds(2_i64.saturating_pow(attempt) * 5_000)
|
||||
}
|
||||
|
||||
type JobResult = Result<(), JobError>;
|
||||
|
||||
@@ -14,6 +14,14 @@ ignore = [
|
||||
# RSA key extraction "Marvin Attack". This is only relevant when using
|
||||
# PKCS#1 v1.5 encryption, which we don't
|
||||
"RUSTSEC-2023-0071",
|
||||
|
||||
# `paste`, as used by `aws-lc-rs` is unmaintained, but we're not concerned
|
||||
# about it having a security vulnerability
|
||||
"RUSTSEC-2024-0436",
|
||||
|
||||
# rust-protobuf has an infinite recursion issue when parsing inputs. We only
|
||||
# use protobuf for opentelemetry output, so we are not affected
|
||||
"RUSTSEC-2024-0437",
|
||||
]
|
||||
|
||||
[licenses]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"clear": "Clear",
|
||||
"close": "Close",
|
||||
"collapse": "Collapse",
|
||||
"confirm": "Confirm",
|
||||
"continue": "Continue",
|
||||
"edit": "Edit",
|
||||
"expand": "Expand",
|
||||
@@ -27,6 +28,7 @@
|
||||
"e2ee": "End-to-end encryption",
|
||||
"loading": "Loading…",
|
||||
"next": "Next",
|
||||
"password": "Password",
|
||||
"previous": "Previous",
|
||||
"saved": "Saved",
|
||||
"saving": "Saving…"
|
||||
@@ -57,7 +59,9 @@
|
||||
"email_field_help": "Add an alternative email you can use to access this account.",
|
||||
"email_field_label": "Add email",
|
||||
"email_in_use_error": "The entered email is already in use",
|
||||
"email_invalid_error": "The entered email is invalid"
|
||||
"email_invalid_error": "The entered email is invalid",
|
||||
"incorrect_password_error": "Incorrect password, please try again",
|
||||
"password_confirmation": "Confirm your account password to add this email address"
|
||||
},
|
||||
"browser_session_details": {
|
||||
"current_badge": "Current"
|
||||
@@ -258,7 +262,9 @@
|
||||
"user_email": {
|
||||
"delete_button_confirmation_modal": {
|
||||
"action": "Delete email",
|
||||
"body": "Delete this email?"
|
||||
"body": "Delete this email?",
|
||||
"incorrect_password": "Incorrect password, please try again",
|
||||
"password_confirmation": "Confirm your account password to delete this email address"
|
||||
},
|
||||
"delete_button_title": "Remove email address",
|
||||
"email": "Email"
|
||||
|
||||
Generated
+91
-91
@@ -12,7 +12,7 @@
|
||||
"@fontsource/inter": "^5.2.5",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@tanstack/react-query": "^5.67.1",
|
||||
"@tanstack/react-query": "^5.67.2",
|
||||
"@tanstack/react-router": "^1.112.18",
|
||||
"@vector-im/compound-design-tokens": "4.0.1",
|
||||
"@vector-im/compound-web": "^7.6.4",
|
||||
@@ -42,9 +42,9 @@
|
||||
"@storybook/react": "^8.6.4",
|
||||
"@storybook/react-vite": "^8.6.4",
|
||||
"@storybook/test": "^8.5.5",
|
||||
"@tanstack/react-query-devtools": "^5.67.1",
|
||||
"@tanstack/react-query-devtools": "^5.67.2",
|
||||
"@tanstack/router-devtools": "^1.112.18",
|
||||
"@tanstack/router-vite-plugin": "^1.112.18",
|
||||
"@tanstack/router-vite-plugin": "^1.112.19",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -53,7 +53,7 @@
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/swagger-ui-dist": "^3.30.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^3.0.7",
|
||||
"@vitest/coverage-v8": "^3.0.8",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"graphql": "^16.10.0",
|
||||
@@ -5328,9 +5328,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.67.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.67.1.tgz",
|
||||
"integrity": "sha512-AkFmuukVejyqVIjEQoFhLb3q+xHl7JG8G9cANWTMe3s8iKzD9j1VBSYXgCjy6vm6xM8cUCR9zP2yqWxY9pTWOA==",
|
||||
"version": "5.67.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.67.2.tgz",
|
||||
"integrity": "sha512-+iaFJ/pt8TaApCk6LuZ0WHS/ECVfTzrxDOEL9HH9Dayyb5OVuomLzDXeSaI2GlGT/8HN7bDGiRXDts3LV+u6ww==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -5338,9 +5338,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-devtools": {
|
||||
"version": "5.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.65.0.tgz",
|
||||
"integrity": "sha512-g5y7zc07U9D3esMdqUfTEVu9kMHoIaVBsD0+M3LPdAdD710RpTcLiNvJY1JkYXqkq9+NV+CQoemVNpQPBXVsJg==",
|
||||
"version": "5.67.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.67.2.tgz",
|
||||
"integrity": "sha512-O4QXFFd7xqp6EX7sdvc9tsVO8nm4lpWBqwpgjpVLW5g7IeOY6VnS/xvs/YzbRhBVkKTMaJMOUGU7NhSX+YGoNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -5349,12 +5349,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.67.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.67.1.tgz",
|
||||
"integrity": "sha512-fH5u4JLwB6A+wLFdi8wWBWAYoJV5deYif2OveJ26ktAWjU499uvVFS1wPWnyEyq5LvZX1MZInvv9QRaIZANRaQ==",
|
||||
"version": "5.67.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.67.2.tgz",
|
||||
"integrity": "sha512-6Sa+BVNJWhAV4QHvIqM73norNeGRWGC3ftN0Ix87cmMvI215I1wyJ44KUTt/9a0V9YimfGcg25AITaYVel71Og==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.67.1"
|
||||
"@tanstack/query-core": "5.67.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -5365,20 +5365,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query-devtools": {
|
||||
"version": "5.67.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.67.1.tgz",
|
||||
"integrity": "sha512-a/2I8ORNalh+ek6Nyb9mEiq2u7vydjVMvaQz5ZieGq7r7DxgIFcPiMs4Ay0qkQvHfptESgXR5nImGTHmmt19yQ==",
|
||||
"version": "5.67.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.67.2.tgz",
|
||||
"integrity": "sha512-cmj2DxBc+/9btQ66n5xI8wTtAma2BLVa403K7zIYiguzJ/kV201jnGensYqJeu1Rd8uRMLLRM74jLVMLDWNRJA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-devtools": "5.65.0"
|
||||
"@tanstack/query-devtools": "5.67.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-query": "^5.67.1",
|
||||
"@tanstack/react-query": "^5.67.2",
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
@@ -5472,9 +5472,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-generator": {
|
||||
"version": "1.112.18",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.112.18.tgz",
|
||||
"integrity": "sha512-CsSnTu0NriD7XhiM4sFBiXdrnn0jTSnY7/vwrmGKIwoA6d7CUsSZSbNa8bUmxGFk221MA4yKajNWjvBWcD0gTg==",
|
||||
"version": "1.112.19",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.112.19.tgz",
|
||||
"integrity": "sha512-JFYj2oAhUuho0B2oAJ37mlVvtt/18EJGE0aNpbWXKfcQihadMqFf4kz/KeYv7kHrMsCtq+n7e50oi5q1x9iYSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5500,9 +5500,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-plugin": {
|
||||
"version": "1.112.18",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.112.18.tgz",
|
||||
"integrity": "sha512-CnAJVwoj9A7ZgTGG9wjynq3FmM2HfDeiKMP7a/sYoEybuJMfMoW+A9lgZe70Dbca55QYQOWdvxfMn3wYss1MPw==",
|
||||
"version": "1.112.19",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.112.19.tgz",
|
||||
"integrity": "sha512-dT+yI/hpxyaW0It1QQfGUSVQzKt1ciVJdqQLuc/blsfqZ7tWIBpLP9fCT5v2c4PIUPkUW/LG1P6bktVukXVKVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5513,7 +5513,7 @@
|
||||
"@babel/traverse": "^7.26.8",
|
||||
"@babel/types": "^7.26.8",
|
||||
"@tanstack/router-core": "^1.112.18",
|
||||
"@tanstack/router-generator": "^1.112.18",
|
||||
"@tanstack/router-generator": "^1.112.19",
|
||||
"@tanstack/router-utils": "^1.112.18",
|
||||
"@tanstack/virtual-file-routes": "^1.99.0",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
@@ -5591,13 +5591,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-vite-plugin": {
|
||||
"version": "1.112.18",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-vite-plugin/-/router-vite-plugin-1.112.18.tgz",
|
||||
"integrity": "sha512-ZSIyFNhzER0atr+0JnejKew5UBZ2TNvHy4C+mH//ax75z5LHY9tUrePoH9uTF+IAZYJcPR54CUCLjXujtHbywQ==",
|
||||
"version": "1.112.19",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-vite-plugin/-/router-vite-plugin-1.112.19.tgz",
|
||||
"integrity": "sha512-es6s+u62wiai2QycxdGUurojKs2hVd7G+Yw5krXE+aKndW7b6EtwhP+O/X01A7AfZU84dDsnvX1AJYVd/VvC6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/router-plugin": "^1.112.18"
|
||||
"@tanstack/router-plugin": "^1.112.19"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -5986,9 +5986,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.7.tgz",
|
||||
"integrity": "sha512-Av8WgBJLTrfLOer0uy3CxjlVuWK4CzcLBndW1Nm2vI+3hZ2ozHututkfc7Blu1u6waeQ7J8gzPK/AsBRnWA5mQ==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.8.tgz",
|
||||
"integrity": "sha512-y7SAKsQirsEJ2F8bulBck4DoluhI2EEgTimHd6EEUgJBGKy9tC25cpywh1MH4FvDGoG2Unt7+asVd1kj4qOSAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6009,8 +6009,8 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "3.0.7",
|
||||
"vitest": "3.0.7"
|
||||
"@vitest/browser": "3.0.8",
|
||||
"vitest": "3.0.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
@@ -6084,13 +6084,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz",
|
||||
"integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.8.tgz",
|
||||
"integrity": "sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.0.7",
|
||||
"@vitest/spy": "3.0.8",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.17"
|
||||
},
|
||||
@@ -6111,9 +6111,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker/node_modules/@vitest/spy": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz",
|
||||
"integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.8.tgz",
|
||||
"integrity": "sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6157,13 +6157,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz",
|
||||
"integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.8.tgz",
|
||||
"integrity": "sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "3.0.7",
|
||||
"@vitest/utils": "3.0.8",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -6171,9 +6171,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner/node_modules/@vitest/pretty-format": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz",
|
||||
"integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.8.tgz",
|
||||
"integrity": "sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6184,13 +6184,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner/node_modules/@vitest/utils": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz",
|
||||
"integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.8.tgz",
|
||||
"integrity": "sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.0.7",
|
||||
"@vitest/pretty-format": "3.0.8",
|
||||
"loupe": "^3.1.3",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
@@ -6199,13 +6199,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz",
|
||||
"integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.8.tgz",
|
||||
"integrity": "sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.0.7",
|
||||
"@vitest/pretty-format": "3.0.8",
|
||||
"magic-string": "^0.30.17",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
@@ -6214,9 +6214,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz",
|
||||
"integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.8.tgz",
|
||||
"integrity": "sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -14363,9 +14363,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz",
|
||||
"integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.8.tgz",
|
||||
"integrity": "sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -14438,19 +14438,19 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz",
|
||||
"integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.8.tgz",
|
||||
"integrity": "sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "3.0.7",
|
||||
"@vitest/mocker": "3.0.7",
|
||||
"@vitest/pretty-format": "^3.0.7",
|
||||
"@vitest/runner": "3.0.7",
|
||||
"@vitest/snapshot": "3.0.7",
|
||||
"@vitest/spy": "3.0.7",
|
||||
"@vitest/utils": "3.0.7",
|
||||
"@vitest/expect": "3.0.8",
|
||||
"@vitest/mocker": "3.0.8",
|
||||
"@vitest/pretty-format": "^3.0.8",
|
||||
"@vitest/runner": "3.0.8",
|
||||
"@vitest/snapshot": "3.0.8",
|
||||
"@vitest/spy": "3.0.8",
|
||||
"@vitest/utils": "3.0.8",
|
||||
"chai": "^5.2.0",
|
||||
"debug": "^4.4.0",
|
||||
"expect-type": "^1.1.0",
|
||||
@@ -14462,7 +14462,7 @@
|
||||
"tinypool": "^1.0.2",
|
||||
"tinyrainbow": "^2.0.0",
|
||||
"vite": "^5.0.0 || ^6.0.0",
|
||||
"vite-node": "3.0.7",
|
||||
"vite-node": "3.0.8",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -14478,8 +14478,8 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"@vitest/browser": "3.0.7",
|
||||
"@vitest/ui": "3.0.7",
|
||||
"@vitest/browser": "3.0.8",
|
||||
"@vitest/ui": "3.0.8",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
@@ -14508,14 +14508,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/@vitest/expect": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz",
|
||||
"integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.8.tgz",
|
||||
"integrity": "sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.0.7",
|
||||
"@vitest/utils": "3.0.7",
|
||||
"@vitest/spy": "3.0.8",
|
||||
"@vitest/utils": "3.0.8",
|
||||
"chai": "^5.2.0",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
@@ -14524,9 +14524,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/@vitest/pretty-format": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz",
|
||||
"integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.8.tgz",
|
||||
"integrity": "sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -14537,9 +14537,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/@vitest/spy": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz",
|
||||
"integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.8.tgz",
|
||||
"integrity": "sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -14550,13 +14550,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/@vitest/utils": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz",
|
||||
"integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.8.tgz",
|
||||
"integrity": "sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.0.7",
|
||||
"@vitest/pretty-format": "3.0.8",
|
||||
"loupe": "^3.1.3",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@fontsource/inter": "^5.2.5",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@tanstack/react-query": "^5.67.1",
|
||||
"@tanstack/react-query": "^5.67.2",
|
||||
"@tanstack/react-router": "^1.112.18",
|
||||
"@vector-im/compound-design-tokens": "4.0.1",
|
||||
"@vector-im/compound-web": "^7.6.4",
|
||||
@@ -52,9 +52,9 @@
|
||||
"@storybook/react": "^8.6.4",
|
||||
"@storybook/react-vite": "^8.6.4",
|
||||
"@storybook/test": "^8.5.5",
|
||||
"@tanstack/react-query-devtools": "^5.67.1",
|
||||
"@tanstack/react-query-devtools": "^5.67.2",
|
||||
"@tanstack/router-devtools": "^1.112.18",
|
||||
"@tanstack/router-vite-plugin": "^1.112.18",
|
||||
"@tanstack/router-vite-plugin": "^1.112.19",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -63,7 +63,7 @@
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/swagger-ui-dist": "^3.30.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^3.0.7",
|
||||
"@vitest/coverage-v8": "^3.0.8",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"graphql": "^16.10.0",
|
||||
|
||||
@@ -1203,6 +1203,11 @@ input RemoveEmailInput {
|
||||
The ID of the email address to remove
|
||||
"""
|
||||
userEmailId: ID!
|
||||
"""
|
||||
The user's current password. This is required if the user is not an
|
||||
admin and it has a password on its account.
|
||||
"""
|
||||
password: String
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -1235,6 +1240,10 @@ enum RemoveEmailStatus {
|
||||
The email address was not found
|
||||
"""
|
||||
NOT_FOUND
|
||||
"""
|
||||
The password provided is incorrect
|
||||
"""
|
||||
INCORRECT_PASSWORD
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -1610,6 +1619,11 @@ input StartEmailAuthenticationInput {
|
||||
"""
|
||||
email: String!
|
||||
"""
|
||||
The user's current password. This is required if the user has a password
|
||||
on its account.
|
||||
"""
|
||||
password: String
|
||||
"""
|
||||
The language to use for the email
|
||||
"""
|
||||
language: String! = "en"
|
||||
@@ -1657,6 +1671,10 @@ enum StartEmailAuthenticationStatus {
|
||||
The email address is already in use on this account
|
||||
"""
|
||||
IN_USE
|
||||
"""
|
||||
The password provided is incorrect
|
||||
"""
|
||||
INCORRECT_PASSWORD
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { Button, Form } from "@vector-im/compound-web";
|
||||
import type React from "react";
|
||||
import { useCallback, useImperativeHandle, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as Dialog from "./Dialog";
|
||||
|
||||
type ModalRef = {
|
||||
prompt: () => Promise<string>;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
destructive?: boolean;
|
||||
ref: React.Ref<ModalRef>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that returns a function that prompts the user to enter a password.
|
||||
* The returned function returns a promise that resolves to the password, and
|
||||
* throws an error if the user cancels the prompt.
|
||||
*
|
||||
* It also returns a ref that must be passed to a mounted Modal component.
|
||||
*/
|
||||
export const usePasswordConfirmation = (): [
|
||||
() => Promise<string>,
|
||||
React.RefObject<ModalRef>,
|
||||
] => {
|
||||
const ref = useRef<ModalRef>({
|
||||
prompt: () => {
|
||||
throw new Error("PasswordConfirmationModal is not mounted!");
|
||||
},
|
||||
});
|
||||
|
||||
const prompt = useCallback(() => ref.current.prompt(), []);
|
||||
|
||||
return [prompt, ref] as const;
|
||||
};
|
||||
|
||||
const PasswordConfirmationModal: React.FC<Props> = ({
|
||||
title,
|
||||
destructive,
|
||||
ref,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const resolversRef = useRef<PromiseWithResolvers<string>>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
prompt: () => {
|
||||
setOpen(true);
|
||||
if (resolversRef.current === null) {
|
||||
resolversRef.current = Promise.withResolvers();
|
||||
}
|
||||
return resolversRef.current.promise;
|
||||
},
|
||||
}));
|
||||
|
||||
const onOpenChange = useCallback((open: boolean) => {
|
||||
setOpen(open);
|
||||
if (!open) {
|
||||
resolversRef.current?.reject(new Error("User cancelled password prompt"));
|
||||
resolversRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.currentTarget);
|
||||
const password = data.get("password");
|
||||
if (typeof password !== "string") {
|
||||
throw new Error(); // This should never happen
|
||||
}
|
||||
resolversRef.current?.resolve(password);
|
||||
resolversRef.current = null;
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog.Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
|
||||
<Form.Root onSubmit={onSubmit}>
|
||||
<Form.Field name="password">
|
||||
<Form.Label>{t("common.password")}</Form.Label>
|
||||
<Form.PasswordControl autoFocus autoComplete="current-password" />
|
||||
</Form.Field>
|
||||
|
||||
<Button type="submit" kind="primary" destructive={destructive}>
|
||||
{t("action.confirm")}
|
||||
</Button>
|
||||
</Form.Root>
|
||||
|
||||
<Dialog.Close asChild>
|
||||
<Button kind="tertiary">{t("action.cancel")}</Button>
|
||||
</Dialog.Close>
|
||||
</Dialog.Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordConfirmationModal;
|
||||
@@ -38,6 +38,7 @@ button[disabled] .user-email-delete-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--cpd-space-4x);
|
||||
border-radius: var(--cpd-space-4x);
|
||||
border: 1px solid var(--cpd-color-gray-400);
|
||||
padding: var(--cpd-space-3x);
|
||||
font: var(--cpd-font-body-md-semibold);
|
||||
|
||||
@@ -7,16 +7,25 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import IconDelete from "@vector-im/compound-design-tokens/assets/web/icons/delete";
|
||||
import IconEmail from "@vector-im/compound-design-tokens/assets/web/icons/email";
|
||||
import { Button, Form, IconButton, Tooltip } from "@vector-im/compound-web";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import {
|
||||
Button,
|
||||
ErrorMessage,
|
||||
Form,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from "@vector-im/compound-web";
|
||||
import { type ReactNode, useCallback, useState } from "react";
|
||||
import { Translation, useTranslation } from "react-i18next";
|
||||
import { type FragmentType, graphql, useFragment } from "../../gql";
|
||||
import { graphqlRequest } from "../../graphql";
|
||||
import { Close, Description, Dialog, Title } from "../Dialog";
|
||||
import LoadingSpinner from "../LoadingSpinner";
|
||||
import PasswordConfirmationModal, {
|
||||
usePasswordConfirmation,
|
||||
} from "../PasswordConfirmation";
|
||||
import styles from "./UserEmail.module.css";
|
||||
|
||||
// This component shows a single user email address, with controls to verify it,
|
||||
// resend the verification email, remove it, and set it as the primary email address.
|
||||
// This component shows a single user email address, with controls to remove it
|
||||
|
||||
export const FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment UserEmail_email on UserEmail {
|
||||
@@ -25,15 +34,9 @@ export const FRAGMENT = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const CONFIG_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment UserEmail_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
}
|
||||
`);
|
||||
|
||||
const REMOVE_EMAIL_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation RemoveEmail($id: ID!) {
|
||||
removeEmail(input: { userEmailId: $id }) {
|
||||
mutation RemoveEmail($id: ID!, $password: String) {
|
||||
removeEmail(input: { userEmailId: $id, password: $password }) {
|
||||
status
|
||||
|
||||
user {
|
||||
@@ -64,92 +67,135 @@ const DeleteButton: React.FC<{ disabled?: boolean; onClick?: () => void }> = ({
|
||||
</Translation>
|
||||
);
|
||||
|
||||
const DeleteButtonWithConfirmation: React.FC<
|
||||
ComponentProps<typeof DeleteButton> & { email: string }
|
||||
> = ({ email, onClick, ...rest }) => {
|
||||
const { t } = useTranslation();
|
||||
const onConfirm = (): void => {
|
||||
onClick?.();
|
||||
};
|
||||
|
||||
// NOOP function, otherwise we dont render a cancel button
|
||||
const onDeny = (): void => {};
|
||||
|
||||
return (
|
||||
<Dialog trigger={<DeleteButton {...rest} />}>
|
||||
<Title>
|
||||
{t("frontend.user_email.delete_button_confirmation_modal.body")}
|
||||
</Title>
|
||||
<Description className={styles.emailModalBox}>
|
||||
<IconEmail />
|
||||
<div>{email}</div>
|
||||
</Description>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Close asChild>
|
||||
<Button
|
||||
kind="primary"
|
||||
destructive
|
||||
onClick={onConfirm}
|
||||
Icon={IconDelete}
|
||||
>
|
||||
{t("frontend.user_email.delete_button_confirmation_modal.action")}
|
||||
</Button>
|
||||
</Close>
|
||||
<Close asChild>
|
||||
<Button kind="tertiary" onClick={onDeny}>
|
||||
{t("action.cancel")}
|
||||
</Button>
|
||||
</Close>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const UserEmail: React.FC<{
|
||||
email: FragmentType<typeof FRAGMENT>;
|
||||
canRemove?: boolean;
|
||||
shouldPromptPassword?: boolean;
|
||||
onRemove?: () => void;
|
||||
}> = ({ email, canRemove, onRemove }) => {
|
||||
}> = ({ email, canRemove, shouldPromptPassword, onRemove }) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const data = useFragment(FRAGMENT, email);
|
||||
const queryClient = useQueryClient();
|
||||
const [promptPassword, passwordConfirmationRef] = usePasswordConfirmation();
|
||||
|
||||
const removeEmail = useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
graphqlRequest({ query: REMOVE_EMAIL_MUTATION, variables: { id } }),
|
||||
onSuccess: (_data) => {
|
||||
onRemove?.();
|
||||
mutationFn: ({ id, password }: { id: string; password?: string }) =>
|
||||
graphqlRequest({
|
||||
query: REMOVE_EMAIL_MUTATION,
|
||||
variables: { id, password },
|
||||
}),
|
||||
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["currentUserGreeting"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["userEmails"] });
|
||||
|
||||
// Don't close the modal unless the mutation was successful removed (or not found)
|
||||
if (
|
||||
data.removeEmail.status !== "NOT_FOUND" &&
|
||||
data.removeEmail.status !== "REMOVED"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
onRemove?.();
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const onRemoveClick = (): void => {
|
||||
removeEmail.mutate(data.id);
|
||||
};
|
||||
const onRemoveClick = useCallback(
|
||||
async (_e: React.MouseEvent<HTMLButtonElement>): Promise<void> => {
|
||||
let password = undefined;
|
||||
if (shouldPromptPassword) {
|
||||
password = await promptPassword();
|
||||
}
|
||||
removeEmail.mutate({ id: data.id, password });
|
||||
},
|
||||
[data.id, promptPassword, shouldPromptPassword, removeEmail.mutate],
|
||||
);
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
// Don't change the modal state if the mutation is pending
|
||||
if (removeEmail.isPending) return;
|
||||
removeEmail.reset();
|
||||
setOpen(open);
|
||||
},
|
||||
[removeEmail.isPending, removeEmail.reset],
|
||||
);
|
||||
|
||||
const status = removeEmail.data?.removeEmail.status ?? null;
|
||||
|
||||
return (
|
||||
<Form.Root>
|
||||
<Form.Field name="email">
|
||||
<Form.Label>{t("frontend.user_email.email")}</Form.Label>
|
||||
<>
|
||||
<PasswordConfirmationModal
|
||||
title={t(
|
||||
"frontend.user_email.delete_button_confirmation_modal.password_confirmation",
|
||||
)}
|
||||
destructive
|
||||
ref={passwordConfirmationRef}
|
||||
/>
|
||||
<Form.Root>
|
||||
<Form.Field name="email">
|
||||
<Form.Label>{t("frontend.user_email.email")}</Form.Label>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Form.TextControl
|
||||
type="email"
|
||||
readOnly
|
||||
value={data.email}
|
||||
className={styles.userEmailField}
|
||||
/>
|
||||
{canRemove && (
|
||||
<DeleteButtonWithConfirmation
|
||||
email={data.email}
|
||||
disabled={removeEmail.isPending}
|
||||
onClick={onRemoveClick}
|
||||
<div className="flex items-center gap-2">
|
||||
<Form.TextControl
|
||||
type="email"
|
||||
readOnly
|
||||
value={data.email}
|
||||
className={styles.userEmailField}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Form.Field>
|
||||
</Form.Root>
|
||||
{canRemove && (
|
||||
<Dialog
|
||||
trigger={<DeleteButton />}
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<Title>
|
||||
{t(
|
||||
"frontend.user_email.delete_button_confirmation_modal.body",
|
||||
)}
|
||||
</Title>
|
||||
<Description className={styles.emailModalBox}>
|
||||
<IconEmail />
|
||||
<div>{data.email}</div>
|
||||
</Description>
|
||||
|
||||
{status === "INCORRECT_PASSWORD" && (
|
||||
<ErrorMessage>
|
||||
{t(
|
||||
"frontend.user_email.delete_button_confirmation_modal.incorrect_password",
|
||||
)}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button
|
||||
kind="primary"
|
||||
type="button"
|
||||
destructive
|
||||
onClick={onRemoveClick}
|
||||
disabled={removeEmail.isPending}
|
||||
Icon={removeEmail.isPending ? undefined : IconDelete}
|
||||
>
|
||||
{!!removeEmail.isPending && <LoadingSpinner inline />}
|
||||
{t(
|
||||
"frontend.user_email.delete_button_confirmation_modal.action",
|
||||
)}
|
||||
</Button>
|
||||
<Close asChild>
|
||||
<Button disabled={removeEmail.isPending} kind="tertiary">
|
||||
{t("action.cancel")}
|
||||
</Button>
|
||||
</Close>
|
||||
</div>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
</Form.Field>
|
||||
</Form.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,13 +10,33 @@ import {
|
||||
ErrorMessage,
|
||||
HelpMessage,
|
||||
} from "@vector-im/compound-web";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { graphql } from "../../gql";
|
||||
import { type FragmentType, graphql, useFragment } from "../../gql";
|
||||
import { graphqlRequest } from "../../graphql";
|
||||
import PasswordConfirmationModal, {
|
||||
usePasswordConfirmation,
|
||||
} from "../PasswordConfirmation";
|
||||
|
||||
export const USER_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment AddEmailForm_user on User {
|
||||
hasPassword
|
||||
}
|
||||
`);
|
||||
|
||||
export const CONFIG_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment AddEmailForm_siteConfig on SiteConfig {
|
||||
passwordLoginEnabled
|
||||
}
|
||||
`);
|
||||
|
||||
const ADD_EMAIL_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation AddEmail($email: String!, $language: String!) {
|
||||
startEmailAuthentication(input: { email: $email, language: $language }) {
|
||||
mutation AddEmail($email: String!, $password: String, $language: String!) {
|
||||
startEmailAuthentication(input: {
|
||||
email: $email,
|
||||
password: $password,
|
||||
language: $language
|
||||
}) {
|
||||
status
|
||||
violations
|
||||
authentication {
|
||||
@@ -28,14 +48,26 @@ const ADD_EMAIL_MUTATION = graphql(/* GraphQL */ `
|
||||
|
||||
const AddEmailForm: React.FC<{
|
||||
onAdd: (id: string) => Promise<void>;
|
||||
}> = ({ onAdd }) => {
|
||||
user: FragmentType<typeof USER_FRAGMENT>;
|
||||
siteConfig: FragmentType<typeof CONFIG_FRAGMENT>;
|
||||
}> = ({ user, siteConfig, onAdd }) => {
|
||||
const { hasPassword } = useFragment(USER_FRAGMENT, user);
|
||||
const { passwordLoginEnabled } = useFragment(CONFIG_FRAGMENT, siteConfig);
|
||||
|
||||
const shouldPromptPassword = hasPassword && passwordLoginEnabled;
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const [promptPassword, passwordConfirmationRef] = usePasswordConfirmation();
|
||||
const addEmail = useMutation({
|
||||
mutationFn: ({ email, language }: { email: string; language: string }) =>
|
||||
mutationFn: ({
|
||||
email,
|
||||
password,
|
||||
language,
|
||||
}: { email: string; password?: string; language: string }) =>
|
||||
graphqlRequest({
|
||||
query: ADD_EMAIL_MUTATION,
|
||||
variables: { email, language },
|
||||
variables: { email, password, language },
|
||||
}),
|
||||
onSuccess: async (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["userEmails"] });
|
||||
@@ -54,62 +86,96 @@ const AddEmailForm: React.FC<{
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (
|
||||
e: React.FormEvent<HTMLFormElement>,
|
||||
): Promise<void> => {
|
||||
e.preventDefault();
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const email = formData.get("input") as string;
|
||||
await addEmail.mutateAsync({ email, language: i18n.languages[0] });
|
||||
};
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const email = formData.get("input") as string;
|
||||
let password = undefined;
|
||||
if (shouldPromptPassword) {
|
||||
password = await promptPassword();
|
||||
}
|
||||
|
||||
const data = await addEmail.mutateAsync({
|
||||
email,
|
||||
password,
|
||||
language: i18n.languages[0],
|
||||
});
|
||||
|
||||
if (data.startEmailAuthentication.status !== "STARTED") {
|
||||
// This is so that the 'Edit in place' component doesn't show a 'Saved' message
|
||||
throw new Error();
|
||||
}
|
||||
},
|
||||
[
|
||||
addEmail.mutateAsync,
|
||||
shouldPromptPassword,
|
||||
promptPassword,
|
||||
i18n.languages,
|
||||
],
|
||||
);
|
||||
|
||||
const status = addEmail.data?.startEmailAuthentication.status ?? null;
|
||||
const violations = addEmail.data?.startEmailAuthentication.violations ?? [];
|
||||
|
||||
return (
|
||||
<EditInPlace
|
||||
onSave={handleSubmit}
|
||||
required
|
||||
type="email"
|
||||
serverInvalid={!!status && status !== "STARTED"}
|
||||
label={t("frontend.add_email_form.email_field_label")}
|
||||
helpLabel={t("frontend.add_email_form.email_field_help")}
|
||||
saveButtonLabel={t("action.save")}
|
||||
savingLabel={t("common.saving")}
|
||||
savedLabel={t("common.saved")}
|
||||
cancelButtonLabel={t("action.cancel")}
|
||||
>
|
||||
<ErrorMessage
|
||||
match="typeMismatch"
|
||||
forceMatch={status === "INVALID_EMAIL_ADDRESS"}
|
||||
<>
|
||||
<PasswordConfirmationModal
|
||||
title={t("frontend.add_email_form.password_confirmation")}
|
||||
ref={passwordConfirmationRef}
|
||||
/>
|
||||
<EditInPlace
|
||||
onSave={handleSubmit}
|
||||
required
|
||||
type="email"
|
||||
serverInvalid={!!status && status !== "STARTED"}
|
||||
label={t("frontend.add_email_form.email_field_label")}
|
||||
helpLabel={t("frontend.add_email_form.email_field_help")}
|
||||
saveButtonLabel={t("action.save")}
|
||||
savingLabel={t("common.saving")}
|
||||
savedLabel={t("common.saved")}
|
||||
cancelButtonLabel={t("action.cancel")}
|
||||
>
|
||||
{t("frontend.add_email_form.email_invalid_error")}
|
||||
</ErrorMessage>
|
||||
|
||||
{status === "IN_USE" && (
|
||||
<ErrorMessage>
|
||||
{t("frontend.add_email_form.email_in_use_error")}
|
||||
<ErrorMessage
|
||||
match="typeMismatch"
|
||||
forceMatch={status === "INVALID_EMAIL_ADDRESS"}
|
||||
>
|
||||
{t("frontend.add_email_form.email_invalid_error")}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
|
||||
{status === "RATE_LIMITED" && (
|
||||
<ErrorMessage>{t("frontend.errors.rate_limit_exceeded")}</ErrorMessage>
|
||||
)}
|
||||
|
||||
{status === "DENIED" && (
|
||||
<>
|
||||
{status === "IN_USE" && (
|
||||
<ErrorMessage>
|
||||
{t("frontend.add_email_form.email_denied_error")}
|
||||
{t("frontend.add_email_form.email_in_use_error")}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
|
||||
{violations.map((violation) => (
|
||||
// XXX: those messages are bad, but it's better to show them than show a generic message
|
||||
<HelpMessage key={violation}>{violation}</HelpMessage>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</EditInPlace>
|
||||
{status === "RATE_LIMITED" && (
|
||||
<ErrorMessage>
|
||||
{t("frontend.errors.rate_limit_exceeded")}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
|
||||
{status === "DENIED" && (
|
||||
<>
|
||||
<ErrorMessage>
|
||||
{t("frontend.add_email_form.email_denied_error")}
|
||||
</ErrorMessage>
|
||||
|
||||
{violations.map((violation) => (
|
||||
// XXX: those messages are bad, but it's better to show them than show a generic message
|
||||
<HelpMessage key={violation}>{violation}</HelpMessage>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{status === "INCORRECT_PASSWORD" && (
|
||||
<ErrorMessage>
|
||||
{t("frontend.add_email_form.incorrect_password_error")}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
</EditInPlace>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -60,16 +60,30 @@ export const query = (pagination: AnyPagination = { first: 6 }) =>
|
||||
}),
|
||||
});
|
||||
|
||||
export const USER_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment UserEmailList_user on User {
|
||||
hasPassword
|
||||
}
|
||||
`);
|
||||
|
||||
export const CONFIG_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment UserEmailList_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
passwordLoginEnabled
|
||||
}
|
||||
`);
|
||||
|
||||
const UserEmailList: React.FC<{
|
||||
siteConfig: FragmentType<typeof CONFIG_FRAGMENT>;
|
||||
}> = ({ siteConfig }) => {
|
||||
const { emailChangeAllowed } = useFragment(CONFIG_FRAGMENT, siteConfig);
|
||||
user: FragmentType<typeof USER_FRAGMENT>;
|
||||
}> = ({ siteConfig, user }) => {
|
||||
const { emailChangeAllowed, passwordLoginEnabled } = useFragment(
|
||||
CONFIG_FRAGMENT,
|
||||
siteConfig,
|
||||
);
|
||||
const { hasPassword } = useFragment(USER_FRAGMENT, user);
|
||||
const shouldPromptPassword = hasPassword && passwordLoginEnabled;
|
||||
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
const [pagination, setPagination] = usePagination();
|
||||
@@ -102,6 +116,7 @@ const UserEmailList: React.FC<{
|
||||
email={edge.node}
|
||||
key={edge.cursor}
|
||||
canRemove={canRemove}
|
||||
shouldPromptPassword={shouldPromptPassword}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
|
||||
+30
-18
@@ -33,16 +33,18 @@ type Documents = {
|
||||
"\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_DetailFragmentDoc,
|
||||
"\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": typeof types.OAuth2Session_DetailFragmentDoc,
|
||||
"\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n": typeof types.UserEmail_EmailFragmentDoc,
|
||||
"\n fragment UserEmail_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": typeof types.UserEmail_SiteConfigFragmentDoc,
|
||||
"\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n": typeof types.RemoveEmailDocument,
|
||||
"\n mutation RemoveEmail($id: ID!, $password: String) {\n removeEmail(input: { userEmailId: $id, password: $password }) {\n status\n\n user {\n id\n }\n }\n }\n": typeof types.RemoveEmailDocument,
|
||||
"\n fragment UserGreeting_user on User {\n id\n matrix {\n mxid\n displayName\n }\n }\n": typeof types.UserGreeting_UserFragmentDoc,
|
||||
"\n fragment UserGreeting_siteConfig on SiteConfig {\n displayNameChangeAllowed\n }\n": typeof types.UserGreeting_SiteConfigFragmentDoc,
|
||||
"\n mutation SetDisplayName($userId: ID!, $displayName: String) {\n setDisplayName(input: { userId: $userId, displayName: $displayName }) {\n status\n }\n }\n": typeof types.SetDisplayNameDocument,
|
||||
"\n mutation AddEmail($email: String!, $language: String!) {\n startEmailAuthentication(input: { email: $email, language: $language }) {\n status\n violations\n authentication {\n id\n }\n }\n }\n": typeof types.AddEmailDocument,
|
||||
"\n fragment AddEmailForm_user on User {\n hasPassword\n }\n": typeof types.AddEmailForm_UserFragmentDoc,
|
||||
"\n fragment AddEmailForm_siteConfig on SiteConfig {\n passwordLoginEnabled\n }\n": typeof types.AddEmailForm_SiteConfigFragmentDoc,
|
||||
"\n mutation AddEmail($email: String!, $password: String, $language: String!) {\n startEmailAuthentication(input: {\n email: $email,\n password: $password,\n language: $language\n }) {\n status\n violations\n authentication {\n id\n }\n }\n }\n": typeof types.AddEmailDocument,
|
||||
"\n query UserEmailList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewer {\n __typename\n ... on User {\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n": typeof types.UserEmailListDocument,
|
||||
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": typeof types.UserEmailList_SiteConfigFragmentDoc,
|
||||
"\n fragment UserEmailList_user on User {\n hasPassword\n }\n": typeof types.UserEmailList_UserFragmentDoc,
|
||||
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n }\n": typeof types.UserEmailList_SiteConfigFragmentDoc,
|
||||
"\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": typeof types.BrowserSessionsOverview_UserFragmentDoc,
|
||||
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": typeof types.UserProfileDocument,
|
||||
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": typeof types.UserProfileDocument,
|
||||
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": typeof types.BrowserSessionListDocument,
|
||||
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": typeof types.SessionsOverviewDocument,
|
||||
"\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": typeof types.AppSessionsListDocument,
|
||||
@@ -82,16 +84,18 @@ const documents: Documents = {
|
||||
"\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_DetailFragmentDoc,
|
||||
"\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": types.OAuth2Session_DetailFragmentDoc,
|
||||
"\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n": types.UserEmail_EmailFragmentDoc,
|
||||
"\n fragment UserEmail_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": types.UserEmail_SiteConfigFragmentDoc,
|
||||
"\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n": types.RemoveEmailDocument,
|
||||
"\n mutation RemoveEmail($id: ID!, $password: String) {\n removeEmail(input: { userEmailId: $id, password: $password }) {\n status\n\n user {\n id\n }\n }\n }\n": types.RemoveEmailDocument,
|
||||
"\n fragment UserGreeting_user on User {\n id\n matrix {\n mxid\n displayName\n }\n }\n": types.UserGreeting_UserFragmentDoc,
|
||||
"\n fragment UserGreeting_siteConfig on SiteConfig {\n displayNameChangeAllowed\n }\n": types.UserGreeting_SiteConfigFragmentDoc,
|
||||
"\n mutation SetDisplayName($userId: ID!, $displayName: String) {\n setDisplayName(input: { userId: $userId, displayName: $displayName }) {\n status\n }\n }\n": types.SetDisplayNameDocument,
|
||||
"\n mutation AddEmail($email: String!, $language: String!) {\n startEmailAuthentication(input: { email: $email, language: $language }) {\n status\n violations\n authentication {\n id\n }\n }\n }\n": types.AddEmailDocument,
|
||||
"\n fragment AddEmailForm_user on User {\n hasPassword\n }\n": types.AddEmailForm_UserFragmentDoc,
|
||||
"\n fragment AddEmailForm_siteConfig on SiteConfig {\n passwordLoginEnabled\n }\n": types.AddEmailForm_SiteConfigFragmentDoc,
|
||||
"\n mutation AddEmail($email: String!, $password: String, $language: String!) {\n startEmailAuthentication(input: {\n email: $email,\n password: $password,\n language: $language\n }) {\n status\n violations\n authentication {\n id\n }\n }\n }\n": types.AddEmailDocument,
|
||||
"\n query UserEmailList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewer {\n __typename\n ... on User {\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n": types.UserEmailListDocument,
|
||||
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": types.UserEmailList_SiteConfigFragmentDoc,
|
||||
"\n fragment UserEmailList_user on User {\n hasPassword\n }\n": types.UserEmailList_UserFragmentDoc,
|
||||
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n }\n": types.UserEmailList_SiteConfigFragmentDoc,
|
||||
"\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": types.BrowserSessionsOverview_UserFragmentDoc,
|
||||
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": types.UserProfileDocument,
|
||||
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": types.UserProfileDocument,
|
||||
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": types.BrowserSessionListDocument,
|
||||
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": types.SessionsOverviewDocument,
|
||||
"\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": types.AppSessionsListDocument,
|
||||
@@ -188,11 +192,7 @@ export function graphql(source: "\n fragment UserEmail_email on UserEmail {\n
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UserEmail_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n"): typeof import('./graphql').UserEmail_SiteConfigFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n"): typeof import('./graphql').RemoveEmailDocument;
|
||||
export function graphql(source: "\n mutation RemoveEmail($id: ID!, $password: String) {\n removeEmail(input: { userEmailId: $id, password: $password }) {\n status\n\n user {\n id\n }\n }\n }\n"): typeof import('./graphql').RemoveEmailDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -208,7 +208,15 @@ export function graphql(source: "\n mutation SetDisplayName($userId: ID!, $disp
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation AddEmail($email: String!, $language: String!) {\n startEmailAuthentication(input: { email: $email, language: $language }) {\n status\n violations\n authentication {\n id\n }\n }\n }\n"): typeof import('./graphql').AddEmailDocument;
|
||||
export function graphql(source: "\n fragment AddEmailForm_user on User {\n hasPassword\n }\n"): typeof import('./graphql').AddEmailForm_UserFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment AddEmailForm_siteConfig on SiteConfig {\n passwordLoginEnabled\n }\n"): typeof import('./graphql').AddEmailForm_SiteConfigFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation AddEmail($email: String!, $password: String, $language: String!) {\n startEmailAuthentication(input: {\n email: $email,\n password: $password,\n language: $language\n }) {\n status\n violations\n authentication {\n id\n }\n }\n }\n"): typeof import('./graphql').AddEmailDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -216,7 +224,11 @@ export function graphql(source: "\n query UserEmailList(\n $first: Int\n
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n"): typeof import('./graphql').UserEmailList_SiteConfigFragmentDoc;
|
||||
export function graphql(source: "\n fragment UserEmailList_user on User {\n hasPassword\n }\n"): typeof import('./graphql').UserEmailList_UserFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n }\n"): typeof import('./graphql').UserEmailList_SiteConfigFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -224,7 +236,7 @@ export function graphql(source: "\n fragment BrowserSessionsOverview_user on Us
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n"): typeof import('./graphql').UserProfileDocument;
|
||||
export function graphql(source: "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n"): typeof import('./graphql').UserProfileDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
+64
-19
@@ -935,6 +935,11 @@ export type QueryUsersArgs = {
|
||||
|
||||
/** The input for the `removeEmail` mutation */
|
||||
export type RemoveEmailInput = {
|
||||
/**
|
||||
* The user's current password. This is required if the user is not an
|
||||
* admin and it has a password on its account.
|
||||
*/
|
||||
password?: InputMaybe<Scalars['String']['input']>;
|
||||
/** The ID of the email address to remove */
|
||||
userEmailId: Scalars['ID']['input'];
|
||||
};
|
||||
@@ -952,6 +957,8 @@ export type RemoveEmailPayload = {
|
||||
|
||||
/** The status of the `removeEmail` mutation */
|
||||
export type RemoveEmailStatus =
|
||||
/** The password provided is incorrect */
|
||||
| 'INCORRECT_PASSWORD'
|
||||
/** The email address was not found */
|
||||
| 'NOT_FOUND'
|
||||
/** The email address was removed */
|
||||
@@ -1190,6 +1197,11 @@ export type StartEmailAuthenticationInput = {
|
||||
email: Scalars['String']['input'];
|
||||
/** The language to use for the email */
|
||||
language?: Scalars['String']['input'];
|
||||
/**
|
||||
* The user's current password. This is required if the user has a password
|
||||
* on its account.
|
||||
*/
|
||||
password?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
/** The payload of the `startEmailAuthentication` mutation */
|
||||
@@ -1207,6 +1219,8 @@ export type StartEmailAuthenticationPayload = {
|
||||
export type StartEmailAuthenticationStatus =
|
||||
/** The email address isn't allowed by the policy */
|
||||
| 'DENIED'
|
||||
/** The password provided is incorrect */
|
||||
| 'INCORRECT_PASSWORD'
|
||||
/** The email address is invalid */
|
||||
| 'INVALID_EMAIL_ADDRESS'
|
||||
/** The email address is already in use on this account */
|
||||
@@ -1640,10 +1654,9 @@ export type OAuth2Session_DetailFragment = (
|
||||
|
||||
export type UserEmail_EmailFragment = { __typename?: 'UserEmail', id: string, email: string } & { ' $fragmentName'?: 'UserEmail_EmailFragment' };
|
||||
|
||||
export type UserEmail_SiteConfigFragment = { __typename?: 'SiteConfig', emailChangeAllowed: boolean } & { ' $fragmentName'?: 'UserEmail_SiteConfigFragment' };
|
||||
|
||||
export type RemoveEmailMutationVariables = Exact<{
|
||||
id: Scalars['ID']['input'];
|
||||
password?: InputMaybe<Scalars['String']['input']>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -1661,8 +1674,13 @@ export type SetDisplayNameMutationVariables = Exact<{
|
||||
|
||||
export type SetDisplayNameMutation = { __typename?: 'Mutation', setDisplayName: { __typename?: 'SetDisplayNamePayload', status: SetDisplayNameStatus } };
|
||||
|
||||
export type AddEmailForm_UserFragment = { __typename?: 'User', hasPassword: boolean } & { ' $fragmentName'?: 'AddEmailForm_UserFragment' };
|
||||
|
||||
export type AddEmailForm_SiteConfigFragment = { __typename?: 'SiteConfig', passwordLoginEnabled: boolean } & { ' $fragmentName'?: 'AddEmailForm_SiteConfigFragment' };
|
||||
|
||||
export type AddEmailMutationVariables = Exact<{
|
||||
email: Scalars['String']['input'];
|
||||
password?: InputMaybe<Scalars['String']['input']>;
|
||||
language: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
@@ -1682,16 +1700,21 @@ export type UserEmailListQuery = { __typename?: 'Query', viewer: { __typename: '
|
||||
& { ' $fragmentRefs'?: { 'UserEmail_EmailFragment': UserEmail_EmailFragment } }
|
||||
) }>, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null, endCursor?: string | null } } } };
|
||||
|
||||
export type UserEmailList_SiteConfigFragment = { __typename?: 'SiteConfig', emailChangeAllowed: boolean } & { ' $fragmentName'?: 'UserEmailList_SiteConfigFragment' };
|
||||
export type UserEmailList_UserFragment = { __typename?: 'User', hasPassword: boolean } & { ' $fragmentName'?: 'UserEmailList_UserFragment' };
|
||||
|
||||
export type UserEmailList_SiteConfigFragment = { __typename?: 'SiteConfig', emailChangeAllowed: boolean, passwordLoginEnabled: boolean } & { ' $fragmentName'?: 'UserEmailList_SiteConfigFragment' };
|
||||
|
||||
export type BrowserSessionsOverview_UserFragment = { __typename?: 'User', id: string, browserSessions: { __typename?: 'BrowserSessionConnection', totalCount: number } } & { ' $fragmentName'?: 'BrowserSessionsOverview_UserFragment' };
|
||||
|
||||
export type UserProfileQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type UserProfileQuery = { __typename?: 'Query', viewerSession: { __typename: 'Anonymous' } | { __typename: 'BrowserSession', id: string, user: { __typename?: 'User', hasPassword: boolean, emails: { __typename?: 'UserEmailConnection', totalCount: number } } } | { __typename: 'Oauth2Session' }, siteConfig: (
|
||||
export type UserProfileQuery = { __typename?: 'Query', viewerSession: { __typename: 'Anonymous' } | { __typename: 'BrowserSession', id: string, user: (
|
||||
{ __typename?: 'User', hasPassword: boolean, emails: { __typename?: 'UserEmailConnection', totalCount: number } }
|
||||
& { ' $fragmentRefs'?: { 'AddEmailForm_UserFragment': AddEmailForm_UserFragment;'UserEmailList_UserFragment': UserEmailList_UserFragment } }
|
||||
) } | { __typename: 'Oauth2Session' }, siteConfig: (
|
||||
{ __typename?: 'SiteConfig', emailChangeAllowed: boolean, passwordLoginEnabled: boolean }
|
||||
& { ' $fragmentRefs'?: { 'UserEmailList_SiteConfigFragment': UserEmailList_SiteConfigFragment;'UserEmail_SiteConfigFragment': UserEmail_SiteConfigFragment;'PasswordChange_SiteConfigFragment': PasswordChange_SiteConfigFragment } }
|
||||
& { ' $fragmentRefs'?: { 'AddEmailForm_SiteConfigFragment': AddEmailForm_SiteConfigFragment;'UserEmailList_SiteConfigFragment': UserEmailList_SiteConfigFragment;'PasswordChange_SiteConfigFragment': PasswordChange_SiteConfigFragment } }
|
||||
) };
|
||||
|
||||
export type BrowserSessionListQueryVariables = Exact<{
|
||||
@@ -2151,11 +2174,6 @@ export const UserEmail_EmailFragmentDoc = new TypedDocumentString(`
|
||||
email
|
||||
}
|
||||
`, {"fragmentName":"UserEmail_email"}) as unknown as TypedDocumentString<UserEmail_EmailFragment, unknown>;
|
||||
export const UserEmail_SiteConfigFragmentDoc = new TypedDocumentString(`
|
||||
fragment UserEmail_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
}
|
||||
`, {"fragmentName":"UserEmail_siteConfig"}) as unknown as TypedDocumentString<UserEmail_SiteConfigFragment, unknown>;
|
||||
export const UserGreeting_UserFragmentDoc = new TypedDocumentString(`
|
||||
fragment UserGreeting_user on User {
|
||||
id
|
||||
@@ -2170,9 +2188,25 @@ export const UserGreeting_SiteConfigFragmentDoc = new TypedDocumentString(`
|
||||
displayNameChangeAllowed
|
||||
}
|
||||
`, {"fragmentName":"UserGreeting_siteConfig"}) as unknown as TypedDocumentString<UserGreeting_SiteConfigFragment, unknown>;
|
||||
export const AddEmailForm_UserFragmentDoc = new TypedDocumentString(`
|
||||
fragment AddEmailForm_user on User {
|
||||
hasPassword
|
||||
}
|
||||
`, {"fragmentName":"AddEmailForm_user"}) as unknown as TypedDocumentString<AddEmailForm_UserFragment, unknown>;
|
||||
export const AddEmailForm_SiteConfigFragmentDoc = new TypedDocumentString(`
|
||||
fragment AddEmailForm_siteConfig on SiteConfig {
|
||||
passwordLoginEnabled
|
||||
}
|
||||
`, {"fragmentName":"AddEmailForm_siteConfig"}) as unknown as TypedDocumentString<AddEmailForm_SiteConfigFragment, unknown>;
|
||||
export const UserEmailList_UserFragmentDoc = new TypedDocumentString(`
|
||||
fragment UserEmailList_user on User {
|
||||
hasPassword
|
||||
}
|
||||
`, {"fragmentName":"UserEmailList_user"}) as unknown as TypedDocumentString<UserEmailList_UserFragment, unknown>;
|
||||
export const UserEmailList_SiteConfigFragmentDoc = new TypedDocumentString(`
|
||||
fragment UserEmailList_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
passwordLoginEnabled
|
||||
}
|
||||
`, {"fragmentName":"UserEmailList_siteConfig"}) as unknown as TypedDocumentString<UserEmailList_SiteConfigFragment, unknown>;
|
||||
export const BrowserSessionsOverview_UserFragmentDoc = new TypedDocumentString(`
|
||||
@@ -2247,8 +2281,8 @@ export const EndOAuth2SessionDocument = new TypedDocumentString(`
|
||||
}
|
||||
`) as unknown as TypedDocumentString<EndOAuth2SessionMutation, EndOAuth2SessionMutationVariables>;
|
||||
export const RemoveEmailDocument = new TypedDocumentString(`
|
||||
mutation RemoveEmail($id: ID!) {
|
||||
removeEmail(input: {userEmailId: $id}) {
|
||||
mutation RemoveEmail($id: ID!, $password: String) {
|
||||
removeEmail(input: {userEmailId: $id, password: $password}) {
|
||||
status
|
||||
user {
|
||||
id
|
||||
@@ -2264,8 +2298,10 @@ export const SetDisplayNameDocument = new TypedDocumentString(`
|
||||
}
|
||||
`) as unknown as TypedDocumentString<SetDisplayNameMutation, SetDisplayNameMutationVariables>;
|
||||
export const AddEmailDocument = new TypedDocumentString(`
|
||||
mutation AddEmail($email: String!, $language: String!) {
|
||||
startEmailAuthentication(input: {email: $email, language: $language}) {
|
||||
mutation AddEmail($email: String!, $password: String, $language: String!) {
|
||||
startEmailAuthentication(
|
||||
input: {email: $email, password: $password, language: $language}
|
||||
) {
|
||||
status
|
||||
violations
|
||||
authentication {
|
||||
@@ -2308,6 +2344,8 @@ export const UserProfileDocument = new TypedDocumentString(`
|
||||
... on BrowserSession {
|
||||
id
|
||||
user {
|
||||
...AddEmailForm_user
|
||||
...UserEmailList_user
|
||||
hasPassword
|
||||
emails(first: 0) {
|
||||
totalCount
|
||||
@@ -2318,19 +2356,26 @@ export const UserProfileDocument = new TypedDocumentString(`
|
||||
siteConfig {
|
||||
emailChangeAllowed
|
||||
passwordLoginEnabled
|
||||
...AddEmailForm_siteConfig
|
||||
...UserEmailList_siteConfig
|
||||
...UserEmail_siteConfig
|
||||
...PasswordChange_siteConfig
|
||||
}
|
||||
}
|
||||
fragment PasswordChange_siteConfig on SiteConfig {
|
||||
passwordChangeAllowed
|
||||
}
|
||||
fragment UserEmail_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
fragment AddEmailForm_user on User {
|
||||
hasPassword
|
||||
}
|
||||
fragment AddEmailForm_siteConfig on SiteConfig {
|
||||
passwordLoginEnabled
|
||||
}
|
||||
fragment UserEmailList_user on User {
|
||||
hasPassword
|
||||
}
|
||||
fragment UserEmailList_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
passwordLoginEnabled
|
||||
}`) as unknown as TypedDocumentString<UserProfileQuery, UserProfileQueryVariables>;
|
||||
export const BrowserSessionListDocument = new TypedDocumentString(`
|
||||
query BrowserSessionList($first: Int, $after: String, $last: Int, $before: String, $lastActive: DateFilter) {
|
||||
@@ -2865,7 +2910,7 @@ export const mockEndOAuth2SessionMutation = (resolver: GraphQLResponseResolver<E
|
||||
* @example
|
||||
* mockRemoveEmailMutation(
|
||||
* ({ query, variables }) => {
|
||||
* const { id } = variables;
|
||||
* const { id, password } = variables;
|
||||
* return HttpResponse.json({
|
||||
* data: { removeEmail }
|
||||
* })
|
||||
@@ -2909,7 +2954,7 @@ export const mockSetDisplayNameMutation = (resolver: GraphQLResponseResolver<Set
|
||||
* @example
|
||||
* mockAddEmailMutation(
|
||||
* ({ query, variables }) => {
|
||||
* const { email, language } = variables;
|
||||
* const { email, password, language } = variables;
|
||||
* return HttpResponse.json({
|
||||
* data: { startEmailAuthentication }
|
||||
* })
|
||||
|
||||
@@ -85,9 +85,18 @@ function Index(): React.ReactElement {
|
||||
defaultOpen
|
||||
title={t("frontend.account.contact_info")}
|
||||
>
|
||||
<UserEmailList siteConfig={siteConfig} />
|
||||
<UserEmailList
|
||||
user={viewerSession.user}
|
||||
siteConfig={siteConfig}
|
||||
/>
|
||||
|
||||
{siteConfig.emailChangeAllowed && <AddEmailForm onAdd={onAdd} />}
|
||||
{siteConfig.emailChangeAllowed && (
|
||||
<AddEmailForm
|
||||
user={viewerSession.user}
|
||||
siteConfig={siteConfig}
|
||||
onAdd={onAdd}
|
||||
/>
|
||||
)}
|
||||
</Collapsible.Section>
|
||||
|
||||
<Separator kind="section" />
|
||||
|
||||
@@ -18,6 +18,8 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
... on BrowserSession {
|
||||
id
|
||||
user {
|
||||
...AddEmailForm_user
|
||||
...UserEmailList_user
|
||||
hasPassword
|
||||
emails(first: 0) {
|
||||
totalCount
|
||||
@@ -29,8 +31,8 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
siteConfig {
|
||||
emailChangeAllowed
|
||||
passwordLoginEnabled
|
||||
...AddEmailForm_siteConfig
|
||||
...UserEmailList_siteConfig
|
||||
...UserEmail_siteConfig
|
||||
...PasswordChange_siteConfig
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,15 @@ import { expect, userEvent, waitFor, within } from "@storybook/test";
|
||||
import i18n from "i18next";
|
||||
import { type GraphQLHandler, HttpResponse } from "msw";
|
||||
import { CONFIG_FRAGMENT as PASSWORD_CHANGE_CONFIG_FRAGMENT } from "../../src/components/AccountManagementPasswordPreview/AccountManagementPasswordPreview";
|
||||
import { FRAGMENT as USER_EMAIL_FRAGMENT } from "../../src/components/UserEmail/UserEmail";
|
||||
import {
|
||||
CONFIG_FRAGMENT as USER_EMAIL_CONFIG_FRAGMENT,
|
||||
FRAGMENT as USER_EMAIL_FRAGMENT,
|
||||
} from "../../src/components/UserEmail/UserEmail";
|
||||
import { CONFIG_FRAGMENT as USER_EMAIL_LIST_CONFIG_FRAGMENT } from "../../src/components/UserProfile/UserEmailList";
|
||||
CONFIG_FRAGMENT as ADD_USER_EMAIL_CONFIG_FRAGMENT,
|
||||
USER_FRAGMENT as ADD_USER_EMAIL_USER_FRAGMENT,
|
||||
} from "../../src/components/UserProfile/AddEmailForm";
|
||||
import {
|
||||
CONFIG_FRAGMENT as USER_EMAIL_LIST_CONFIG_FRAGMENT,
|
||||
USER_FRAGMENT as USER_EMAIL_LIST_USER_FRAGMENT,
|
||||
} from "../../src/components/UserProfile/UserEmailList";
|
||||
import { makeFragmentData } from "../../src/gql";
|
||||
import {
|
||||
mockUserEmailListQuery,
|
||||
@@ -48,12 +52,26 @@ const userProfileHandler = ({
|
||||
viewerSession: {
|
||||
__typename: "BrowserSession",
|
||||
id: "session-id",
|
||||
user: {
|
||||
hasPassword,
|
||||
emails: {
|
||||
totalCount: emailTotalCount,
|
||||
user: Object.assign(
|
||||
{
|
||||
hasPassword,
|
||||
emails: {
|
||||
totalCount: emailTotalCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
makeFragmentData(
|
||||
{
|
||||
hasPassword,
|
||||
},
|
||||
ADD_USER_EMAIL_USER_FRAGMENT,
|
||||
),
|
||||
makeFragmentData(
|
||||
{
|
||||
hasPassword,
|
||||
},
|
||||
USER_EMAIL_LIST_USER_FRAGMENT,
|
||||
),
|
||||
),
|
||||
},
|
||||
|
||||
siteConfig: Object.assign(
|
||||
@@ -64,12 +82,14 @@ const userProfileHandler = ({
|
||||
makeFragmentData(
|
||||
{
|
||||
emailChangeAllowed,
|
||||
passwordLoginEnabled,
|
||||
},
|
||||
USER_EMAIL_CONFIG_FRAGMENT,
|
||||
ADD_USER_EMAIL_CONFIG_FRAGMENT,
|
||||
),
|
||||
makeFragmentData(
|
||||
{
|
||||
emailChangeAllowed,
|
||||
passwordLoginEnabled,
|
||||
},
|
||||
USER_EMAIL_LIST_CONFIG_FRAGMENT,
|
||||
),
|
||||
|
||||
@@ -6,15 +6,19 @@
|
||||
import { HttpResponse } from "msw";
|
||||
import { CONFIG_FRAGMENT as PASSWORD_CHANGE_CONFIG_FRAGMENT } from "../../src/components/AccountManagementPasswordPreview/AccountManagementPasswordPreview";
|
||||
import { FRAGMENT as FOOTER_FRAGMENT } from "../../src/components/Footer/Footer";
|
||||
import {
|
||||
CONFIG_FRAGMENT as USER_EMAIL_CONFIG_FRAGMENT,
|
||||
FRAGMENT as USER_EMAIL_FRAGMENT,
|
||||
} from "../../src/components/UserEmail/UserEmail";
|
||||
import { FRAGMENT as USER_EMAIL_FRAGMENT } from "../../src/components/UserEmail/UserEmail";
|
||||
import {
|
||||
CONFIG_FRAGMENT as USER_GREETING_CONFIG_FRAGMENT,
|
||||
FRAGMENT as USER_GREETING_FRAGMENT,
|
||||
} from "../../src/components/UserGreeting/UserGreeting";
|
||||
import { CONFIG_FRAGMENT as USER_EMAIL_LIST_CONFIG_FRAGMENT } from "../../src/components/UserProfile/UserEmailList";
|
||||
import {
|
||||
CONFIG_FRAGMENT as ADD_USER_EMAIL_CONFIG_FRAGMENT,
|
||||
USER_FRAGMENT as ADD_USER_EMAIL_USER_FRAGMENT,
|
||||
} from "../../src/components/UserProfile/AddEmailForm";
|
||||
import {
|
||||
CONFIG_FRAGMENT as USER_EMAIL_LIST_CONFIG_FRAGMENT,
|
||||
USER_FRAGMENT as USER_EMAIL_LIST_USER_FRAGMENT,
|
||||
} from "../../src/components/UserProfile/UserEmailList";
|
||||
import { makeFragmentData } from "../../src/gql";
|
||||
import {
|
||||
mockCurrentUserGreetingQuery,
|
||||
@@ -90,12 +94,26 @@ export const handlers = [
|
||||
viewerSession: {
|
||||
__typename: "BrowserSession",
|
||||
id: "browser-session-id",
|
||||
user: {
|
||||
hasPassword: true,
|
||||
emails: {
|
||||
totalCount: 1,
|
||||
user: Object.assign(
|
||||
{
|
||||
hasPassword: true,
|
||||
emails: {
|
||||
totalCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
makeFragmentData(
|
||||
{
|
||||
hasPassword: true,
|
||||
},
|
||||
ADD_USER_EMAIL_USER_FRAGMENT,
|
||||
),
|
||||
makeFragmentData(
|
||||
{
|
||||
hasPassword: true,
|
||||
},
|
||||
USER_EMAIL_LIST_USER_FRAGMENT,
|
||||
),
|
||||
),
|
||||
},
|
||||
|
||||
siteConfig: Object.assign(
|
||||
@@ -106,12 +124,14 @@ export const handlers = [
|
||||
makeFragmentData(
|
||||
{
|
||||
emailChangeAllowed: true,
|
||||
passwordLoginEnabled: true,
|
||||
},
|
||||
USER_EMAIL_CONFIG_FRAGMENT,
|
||||
ADD_USER_EMAIL_CONFIG_FRAGMENT,
|
||||
),
|
||||
makeFragmentData(
|
||||
{
|
||||
emailChangeAllowed: true,
|
||||
passwordLoginEnabled: true,
|
||||
},
|
||||
USER_EMAIL_LIST_CONFIG_FRAGMENT,
|
||||
),
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
|
||||
exports[`Account home page > display name edit box > displays an error if the display name is invalid 1`] = `
|
||||
<div
|
||||
aria-describedby="radix-:r72:"
|
||||
aria-labelledby="radix-:r71:"
|
||||
aria-describedby="radix-:r7q:"
|
||||
aria-labelledby="radix-:r7p:"
|
||||
class="_body_9cf7b0"
|
||||
data-state="open"
|
||||
id="radix-:r70:"
|
||||
id="radix-:r7o:"
|
||||
role="dialog"
|
||||
style="pointer-events: auto;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<h2
|
||||
class="_title_9cf7b0"
|
||||
id="radix-:r71:"
|
||||
id="radix-:r7p:"
|
||||
>
|
||||
Edit profile
|
||||
</h2>
|
||||
@@ -40,29 +40,29 @@ exports[`Account home page > display name edit box > displays an error if the di
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
data-invalid="true"
|
||||
for="radix-:r8c:"
|
||||
for="radix-:r9a:"
|
||||
>
|
||||
Display name
|
||||
</label>
|
||||
<div
|
||||
class="_container_1s836_8"
|
||||
id=":r8d:"
|
||||
id=":r9b:"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-:r8j:"
|
||||
aria-describedby="radix-:r9h:"
|
||||
aria-invalid="true"
|
||||
autocomplete="name"
|
||||
class="_control_sqdq4_10 _control_1s836_13"
|
||||
data-invalid="true"
|
||||
id="radix-:r8c:"
|
||||
id="radix-:r9a:"
|
||||
name="displayname"
|
||||
title=""
|
||||
type="text"
|
||||
value="Alice"
|
||||
/>
|
||||
<button
|
||||
aria-controls=":r8d:"
|
||||
aria-labelledby=":r8e:"
|
||||
aria-controls=":r9b:"
|
||||
aria-labelledby=":r9c:"
|
||||
class="_action_1s836_24"
|
||||
type="button"
|
||||
>
|
||||
@@ -82,7 +82,7 @@ exports[`Account home page > display name edit box > displays an error if the di
|
||||
</div>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-:r8j:"
|
||||
id="radix-:r9h:"
|
||||
>
|
||||
This is what others will see wherever you’re signed in.
|
||||
</span>
|
||||
@@ -92,13 +92,13 @@ exports[`Account home page > display name edit box > displays an error if the di
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-:r8k:"
|
||||
for="radix-:r9i:"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
class="_control_sqdq4_10"
|
||||
id="radix-:r8k:"
|
||||
id="radix-:r9i:"
|
||||
name="mxid"
|
||||
readonly=""
|
||||
title=""
|
||||
@@ -129,7 +129,7 @@ exports[`Account home page > display name edit box > displays an error if the di
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby=":r8l:"
|
||||
aria-labelledby=":r9j:"
|
||||
class="_close_9cf7b0"
|
||||
type="button"
|
||||
>
|
||||
@@ -150,18 +150,18 @@ exports[`Account home page > display name edit box > displays an error if the di
|
||||
|
||||
exports[`Account home page > display name edit box > lets edit the display name 1`] = `
|
||||
<div
|
||||
aria-describedby="radix-:r1e:"
|
||||
aria-labelledby="radix-:r1d:"
|
||||
aria-describedby="radix-:r1k:"
|
||||
aria-labelledby="radix-:r1j:"
|
||||
class="_body_9cf7b0"
|
||||
data-state="open"
|
||||
id="radix-:r1c:"
|
||||
id="radix-:r1i:"
|
||||
role="dialog"
|
||||
style="pointer-events: auto;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<h2
|
||||
class="_title_9cf7b0"
|
||||
id="radix-:r1d:"
|
||||
id="radix-:r1j:"
|
||||
>
|
||||
Edit profile
|
||||
</h2>
|
||||
@@ -186,27 +186,27 @@ exports[`Account home page > display name edit box > lets edit the display name
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-:r2o:"
|
||||
for="radix-:r34:"
|
||||
>
|
||||
Display name
|
||||
</label>
|
||||
<div
|
||||
class="_container_1s836_8"
|
||||
id=":r2p:"
|
||||
id=":r35:"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-:r2v:"
|
||||
aria-describedby="radix-:r3b:"
|
||||
autocomplete="name"
|
||||
class="_control_sqdq4_10 _control_1s836_13"
|
||||
id="radix-:r2o:"
|
||||
id="radix-:r34:"
|
||||
name="displayname"
|
||||
title=""
|
||||
type="text"
|
||||
value="Alice"
|
||||
/>
|
||||
<button
|
||||
aria-controls=":r2p:"
|
||||
aria-labelledby=":r2q:"
|
||||
aria-controls=":r35:"
|
||||
aria-labelledby=":r36:"
|
||||
class="_action_1s836_24"
|
||||
type="button"
|
||||
>
|
||||
@@ -226,7 +226,7 @@ exports[`Account home page > display name edit box > lets edit the display name
|
||||
</div>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-:r2v:"
|
||||
id="radix-:r3b:"
|
||||
>
|
||||
This is what others will see wherever you’re signed in.
|
||||
</span>
|
||||
@@ -236,13 +236,13 @@ exports[`Account home page > display name edit box > lets edit the display name
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-:r30:"
|
||||
for="radix-:r3c:"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
class="_control_sqdq4_10"
|
||||
id="radix-:r30:"
|
||||
id="radix-:r3c:"
|
||||
name="mxid"
|
||||
readonly=""
|
||||
title=""
|
||||
@@ -273,7 +273,7 @@ exports[`Account home page > display name edit box > lets edit the display name
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby=":r31:"
|
||||
aria-labelledby=":r3d:"
|
||||
class="_close_9cf7b0"
|
||||
type="button"
|
||||
>
|
||||
@@ -463,7 +463,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-:rg:"
|
||||
for="radix-:rj:"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
@@ -472,7 +472,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
>
|
||||
<input
|
||||
class="_control_sqdq4_10 _userEmailField_e2a518"
|
||||
id="radix-:rg:"
|
||||
id="radix-:rj:"
|
||||
name="email"
|
||||
readonly=""
|
||||
title=""
|
||||
@@ -490,7 +490,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-:rh:"
|
||||
for="radix-:rn:"
|
||||
>
|
||||
Add email
|
||||
</label>
|
||||
@@ -498,9 +498,9 @@ exports[`Account home page > renders the page 1`] = `
|
||||
class="_controls_17lij_8"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-:ri:"
|
||||
aria-describedby="radix-:ro:"
|
||||
class="_control_sqdq4_10"
|
||||
id="radix-:rh:"
|
||||
id="radix-:rn:"
|
||||
name="input"
|
||||
required=""
|
||||
title=""
|
||||
@@ -509,7 +509,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
</div>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-:ri:"
|
||||
id="radix-:ro:"
|
||||
>
|
||||
Add an alternative email you can use to access this account.
|
||||
</span>
|
||||
@@ -524,7 +524,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
role="separator"
|
||||
/>
|
||||
<section
|
||||
aria-labelledby=":rj:"
|
||||
aria-labelledby=":rp:"
|
||||
class="_root_f1daaa"
|
||||
data-state="open"
|
||||
>
|
||||
@@ -536,14 +536,14 @@ exports[`Account home page > renders the page 1`] = `
|
||||
>
|
||||
<h4
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 _triggerTitle_f1daaa"
|
||||
id=":rj:"
|
||||
id=":rp:"
|
||||
>
|
||||
Account password
|
||||
</h4>
|
||||
<button
|
||||
aria-controls="radix-:rl:"
|
||||
aria-controls="radix-:rr:"
|
||||
aria-expanded="true"
|
||||
aria-labelledby=":rm:"
|
||||
aria-labelledby=":rs:"
|
||||
class="_icon-button_m2erp_8 _triggerIcon_f1daaa"
|
||||
data-state="open"
|
||||
role="button"
|
||||
@@ -573,7 +573,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
<article
|
||||
class="_content_f1daaa"
|
||||
data-state="open"
|
||||
id="radix-:rl:"
|
||||
id="radix-:rr:"
|
||||
style="transition-duration: 0s; animation-name: none;"
|
||||
>
|
||||
<form
|
||||
@@ -584,14 +584,14 @@ exports[`Account home page > renders the page 1`] = `
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-:rr:"
|
||||
for="radix-:r11:"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby="radix-:rs:"
|
||||
aria-describedby="radix-:r12:"
|
||||
class="_control_sqdq4_10"
|
||||
id="radix-:rr:"
|
||||
id="radix-:r11:"
|
||||
name="password_preview"
|
||||
readonly=""
|
||||
title=""
|
||||
@@ -600,7 +600,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
/>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-:rs:"
|
||||
id="radix-:r12:"
|
||||
>
|
||||
<a
|
||||
class="_link_7634c3"
|
||||
@@ -620,7 +620,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
role="separator"
|
||||
/>
|
||||
<section
|
||||
aria-labelledby=":rt:"
|
||||
aria-labelledby=":r13:"
|
||||
class="_root_f1daaa"
|
||||
data-state="closed"
|
||||
>
|
||||
@@ -632,14 +632,14 @@ exports[`Account home page > renders the page 1`] = `
|
||||
>
|
||||
<h4
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 _triggerTitle_f1daaa"
|
||||
id=":rt:"
|
||||
id=":r13:"
|
||||
>
|
||||
End-to-end encryption
|
||||
</h4>
|
||||
<button
|
||||
aria-controls="radix-:rv:"
|
||||
aria-controls="radix-:r15:"
|
||||
aria-expanded="false"
|
||||
aria-labelledby=":r10:"
|
||||
aria-labelledby=":r16:"
|
||||
class="_icon-button_m2erp_8 _triggerIcon_f1daaa"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
@@ -675,7 +675,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="radix-:r15:"
|
||||
aria-controls="radix-:r1b:"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _destructive_vczzf_107"
|
||||
|
||||
Reference in New Issue
Block a user