From bb547487160ac208bcd238d4f18ff5ad6e2c1a0c Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Mon, 16 Feb 2026 16:07:18 +0700 Subject: [PATCH] feat: Redesign workspace join (#10507) * Redesign workspace join Signed-off-by: Artem Savchenko * Login rename Signed-off-by: Artem Savchenko * Fix UI tests Signed-off-by: Artem Savchenko * Fix unstable tests Signed-off-by: Artem Savchenko * Minor fixes Signed-off-by: Artem Savchenko * Clean up Signed-off-by: Artem Savchenko * Fix translations Signed-off-by: Artem Savchenko * Redesign join Signed-off-by: Artem Savchenko --------- Signed-off-by: Artem Savchenko --- .../packages/account-client/src/client.ts | 21 ++ .../core/packages/account-client/src/types.ts | 5 + plugins/login-assets/lang/cs.json | 10 +- plugins/login-assets/lang/de.json | 10 +- plugins/login-assets/lang/en.json | 10 +- plugins/login-assets/lang/es.json | 10 +- plugins/login-assets/lang/fr.json | 10 +- plugins/login-assets/lang/it.json | 10 +- plugins/login-assets/lang/ja.json | 10 +- plugins/login-assets/lang/pt-br.json | 10 +- plugins/login-assets/lang/pt.json | 10 +- plugins/login-assets/lang/ru.json | 10 +- plugins/login-assets/lang/tr.json | 10 +- plugins/login-assets/lang/zh.json | 10 +- .../src/components/Form.svelte | 7 +- .../src/components/Join.svelte | 253 ++++++++++++++++-- plugins/login-resources/src/plugin.ts | 10 +- plugins/login-resources/src/utils.ts | 30 ++- server/account/src/operations.ts | 79 +++++- server/account/src/types.ts | 4 + tests/sanity/tests/model/signin-page.ts | 29 +- 21 files changed, 514 insertions(+), 44 deletions(-) diff --git a/foundations/core/packages/account-client/src/client.ts b/foundations/core/packages/account-client/src/client.ts index a974028cf3..14a7379ac8 100644 --- a/foundations/core/packages/account-client/src/client.ts +++ b/foundations/core/packages/account-client/src/client.ts @@ -42,6 +42,7 @@ import type { LoginInfo, LoginInfoByToken, LoginInfoRequestData, + InviteInfo, LoginInfoWithWorkspaces, MailboxInfo, MailboxOptions, @@ -124,7 +125,9 @@ export interface AccountClient { } ) => Promise checkJoin: (inviteId: string) => Promise + joinByToken: (inviteId: string) => Promise checkAutoJoin: (inviteId: string, firstName?: string, lastName?: string) => Promise + getInviteInfo: (inviteId: string) => Promise getWorkspaceInfo: (updateLastVisit?: boolean) => Promise getWorkspacesInfo: (workspaces: WorkspaceUuid[]) => Promise updateLastVisit: (workspaces: WorkspaceUuid[]) => Promise @@ -577,6 +580,15 @@ class AccountClientImpl implements AccountClient { return await this.rpc(request) } + async joinByToken (inviteId: string): Promise { + const request = { + method: 'joinByToken' as const, + params: { inviteId } + } + + return await this.rpc(request) + } + async checkAutoJoin (inviteId: string, firstName?: string, lastName?: string): Promise { const request = { method: 'checkAutoJoin' as const, @@ -586,6 +598,15 @@ class AccountClientImpl implements AccountClient { return await this.rpc(request) } + async getInviteInfo (inviteId: string): Promise { + const request = { + method: 'getInviteInfo' as const, + params: { inviteId } + } + + return await this.rpc(request) + } + async getWorkspacesInfo (ids: WorkspaceUuid[]): Promise { const request = { method: 'getWorkspacesInfo' as const, diff --git a/foundations/core/packages/account-client/src/types.ts b/foundations/core/packages/account-client/src/types.ts index a267ae06b9..f055a74d70 100644 --- a/foundations/core/packages/account-client/src/types.ts +++ b/foundations/core/packages/account-client/src/types.ts @@ -80,6 +80,11 @@ export interface WorkspaceInviteInfo { name?: string } +/** Public invite details from getInviteInfo (no auth required). */ +export interface InviteInfo { + workspaceName: string | null +} + export interface OtpInfo { sent: boolean retryOn: Timestamp diff --git a/plugins/login-assets/lang/cs.json b/plugins/login-assets/lang/cs.json index 2603d4038a..60266628ad 100644 --- a/plugins/login-assets/lang/cs.json +++ b/plugins/login-assets/lang/cs.json @@ -20,6 +20,7 @@ "LastName": "Příjmení", "FirstName": "Jméno", "Join": "Připojit se", + "JoinWorkspace": "Připojit se k {workspaceName}", "Email": "E-mail", "Password": "Heslo", "Workspace": "Název pracovního prostoru", @@ -84,6 +85,13 @@ "SignUpToCreateWorkspace": "Neexistují žádné pracovní prostory s přístupem pro hosty. Prosím, zaregistrujte se a vytvořte si vlastní.", "WhatIsYourName": "Jak se jmenujete?", "AccessExpired": "Doba přístupu vypršela. Požádejte prosím o nový odkaz pro přístup.", - "AccessNotActive": "Doba přístupu ještě nezačala. Začíná za: " + "AccessNotActive": "Doba přístupu ještě nezačala. Začíná za: ", + "JoinWithThisAccount": "Připojit se tímto účtem", + "UseDifferentAccount": "Použít jiný účet", + "UseCurrentAccountToJoin": "Už jste přihlášeni?", + "LogInAndJoin": "Přihlásit se a připojit", + "SignUpAndJoin": "Registrovat se a připojit", + "CreateNewAccount": "Vytvořit nový účet", + "SignedInAs": "Přihlášen jako {name}" } } diff --git a/plugins/login-assets/lang/de.json b/plugins/login-assets/lang/de.json index 465b94e698..ddfa3e4e04 100644 --- a/plugins/login-assets/lang/de.json +++ b/plugins/login-assets/lang/de.json @@ -20,6 +20,7 @@ "LastName": "Nachname", "FirstName": "Vorname", "Join": "Beitreten", + "JoinWorkspace": "{workspaceName} beitreten", "Email": "E-Mail", "Password": "Passwort", "Workspace": "Name des Arbeitsbereichs", @@ -84,6 +85,13 @@ "SignUpToCreateWorkspace": "Keine Arbeitsbereiche mit Gastzugang. Bitte registrieren Sie sich, um Ihren eigenen zu erstellen.", "WhatIsYourName": "Wie ist Ihr Name?", "AccessExpired": "Zugriffszeit abgelaufen. Bitte fragen Sie nach einem neuen Zugangslink.", - "AccessNotActive": "Zugriffszeit hat noch nicht begonnen. Sie beginnt in: " + "AccessNotActive": "Zugriffszeit hat noch nicht begonnen. Sie beginnt in: ", + "JoinWithThisAccount": "Mit diesem Konto beitreten", + "UseDifferentAccount": "Anderes Konto verwenden", + "UseCurrentAccountToJoin": "Bereits angemeldet?", + "LogInAndJoin": "Anmelden & beitreten", + "SignUpAndJoin": "Registrieren & beitreten", + "CreateNewAccount": "Neues Konto erstellen", + "SignedInAs": "Angemeldet als {name}" } } diff --git a/plugins/login-assets/lang/en.json b/plugins/login-assets/lang/en.json index 244d612ed8..4b62fb8738 100644 --- a/plugins/login-assets/lang/en.json +++ b/plugins/login-assets/lang/en.json @@ -20,6 +20,7 @@ "LastName": "Last name", "FirstName": "First name", "Join": "Join", + "JoinWorkspace": "Join {workspaceName}", "Email": "Email", "Password": "Password", "Workspace": "Workspace name", @@ -83,6 +84,13 @@ "SignUpToCreateWorkspace": "There are no workspaces with guest access. Please sign up to create your own.", "WhatIsYourName": "What is your name?", "AccessExpired": "Access time expired. Please ask for a new access link.", - "AccessNotActive": "Access time hasn't started yet. It is starting in: " + "AccessNotActive": "Access time hasn't started yet. It is starting in: ", + "JoinWithThisAccount": "Join with this account", + "UseDifferentAccount": "Use different account", + "UseCurrentAccountToJoin": "Already signed in?", + "LogInAndJoin": "Log In & Join", + "SignUpAndJoin": "Sign Up & Join", + "CreateNewAccount": "Create new account", + "SignedInAs": "Signed in as {name}" } } diff --git a/plugins/login-assets/lang/es.json b/plugins/login-assets/lang/es.json index 8d584374b0..fa2eb6f9f4 100644 --- a/plugins/login-assets/lang/es.json +++ b/plugins/login-assets/lang/es.json @@ -20,6 +20,7 @@ "LastName": "Apellido", "FirstName": "Nombre", "Join": "Unirse", + "JoinWorkspace": "Unirse a {workspaceName}", "Email": "Correo Electrónico", "Password": "Contraseña", "Workspace": "Nombre del espacio de trabajo", @@ -83,6 +84,13 @@ "SignUpToCreateWorkspace": "No hay espacios de trabajo con acceso de invitado. Por favor, regístrese para crear el suyo.", "WhatIsYourName": "¿Cuál es su nombre?", "AccessExpired": "El tiempo de acceso ha expirado. Por favor, solicite un nuevo enlace de acceso.", - "AccessNotActive": "El tiempo de acceso aún no ha comenzado. Comienza en: " + "AccessNotActive": "El tiempo de acceso aún no ha comenzado. Comienza en: ", + "JoinWithThisAccount": "Unirse con esta cuenta", + "UseDifferentAccount": "Usar otra cuenta", + "UseCurrentAccountToJoin": "¿Ya has iniciado sesión?", + "LogInAndJoin": "Iniciar sesión y unirse", + "SignUpAndJoin": "Registrarse y unirse", + "CreateNewAccount": "Crear nueva cuenta", + "SignedInAs": "Conectado como {name}" } } diff --git a/plugins/login-assets/lang/fr.json b/plugins/login-assets/lang/fr.json index b8d6751927..38d3f5ad27 100644 --- a/plugins/login-assets/lang/fr.json +++ b/plugins/login-assets/lang/fr.json @@ -20,6 +20,7 @@ "LastName": "Nom de famille", "FirstName": "Prénom", "Join": "Rejoindre", + "JoinWorkspace": "Rejoindre {workspaceName}", "Email": "E-mail", "Password": "Mot de passe", "Workspace": "Nom de l'espace de travail", @@ -83,6 +84,13 @@ "SignUpToCreateWorkspace": "Aucun espace de travail avec accès invité. Veuillez vous inscrire pour créer le vôtre.", "WhatIsYourName": "Quel est votre nom ?", "AccessExpired": "Le temps d'accès a expiré. Veuillez demander un nouveau lien d'accès.", - "AccessNotActive": "Le temps d'accès n'a pas encore commencé. Il commence dans : " + "AccessNotActive": "Le temps d'accès n'a pas encore commencé. Il commence dans : ", + "JoinWithThisAccount": "Rejoindre avec ce compte", + "UseDifferentAccount": "Utiliser un autre compte", + "UseCurrentAccountToJoin": "Déjà connecté ?", + "LogInAndJoin": "Se connecter et rejoindre", + "SignUpAndJoin": "S'inscrire et rejoindre", + "CreateNewAccount": "Créer un nouveau compte", + "SignedInAs": "Connecté en tant que {name}" } } diff --git a/plugins/login-assets/lang/it.json b/plugins/login-assets/lang/it.json index b9a434cc38..9f3e2dd742 100644 --- a/plugins/login-assets/lang/it.json +++ b/plugins/login-assets/lang/it.json @@ -20,6 +20,7 @@ "LastName": "Cognome", "FirstName": "Nome", "Join": "Unisciti", + "JoinWorkspace": "Partecipa a {workspaceName}", "Email": "Email", "Password": "Password", "Workspace": "Nome dello spazio di lavoro", @@ -83,6 +84,13 @@ "SignUpToCreateWorkspace": "Nessuna area di lavoro con accesso ospite. Per favore, registrati per crearne una tua.", "WhatIsYourName": "Come ti chiami?", "AccessExpired": "Il tempo di accesso è scaduto. Per favore, richiedi un nuovo link di accesso.", - "AccessNotActive": "Il tempo di accesso non è ancora iniziato. Inizia tra: " + "AccessNotActive": "Il tempo di accesso non è ancora iniziato. Inizia tra: ", + "JoinWithThisAccount": "Partecipa con questo account", + "UseDifferentAccount": "Usa un altro account", + "UseCurrentAccountToJoin": "Già effettuato l'accesso?", + "LogInAndJoin": "Accedi e partecipa", + "SignUpAndJoin": "Iscriviti e partecipa", + "CreateNewAccount": "Crea nuovo account", + "SignedInAs": "Accesso effettuato come {name}" } } diff --git a/plugins/login-assets/lang/ja.json b/plugins/login-assets/lang/ja.json index f20b7f9b2a..21d790ef07 100644 --- a/plugins/login-assets/lang/ja.json +++ b/plugins/login-assets/lang/ja.json @@ -13,6 +13,7 @@ "LastName": "姓", "FirstName": "名", "Join": "参加", + "JoinWorkspace": "{workspaceName} に参加", "Email": "メールアドレス", "Password": "パスワード", "Workspace": "ワークスペース名", @@ -83,6 +84,13 @@ "EnterCurrentPassword": "現在のパスワードを入力", "EnterNewPassword": "新しいパスワードを入力", "RepeatNewPassword": "新しいパスワードを再入力", - "PasswordExpiredDesc": "パスワードの有効期限が切れています。続行するには新しいパスワードを設定してください。" + "PasswordExpiredDesc": "パスワードの有効期限が切れています。続行するには新しいパスワードを設定してください。", + "JoinWithThisAccount": "このアカウントで参加", + "UseDifferentAccount": "別のアカウントを使用", + "UseCurrentAccountToJoin": "すでにサインインしていますか?", + "LogInAndJoin": "ログインして参加", + "SignUpAndJoin": "サインアップして参加", + "CreateNewAccount": "新規アカウントを作成", + "SignedInAs": "{name} としてサインイン中" } } diff --git a/plugins/login-assets/lang/pt-br.json b/plugins/login-assets/lang/pt-br.json index a997db0191..a722f2dce0 100644 --- a/plugins/login-assets/lang/pt-br.json +++ b/plugins/login-assets/lang/pt-br.json @@ -20,6 +20,7 @@ "LastName": "Apelido", "FirstName": "Nome", "Join": "Participar", + "JoinWorkspace": "Participar de {workspaceName}", "Email": "Email", "Password": "Palavra-Passe", "Workspace": "Nome do espaço de trabalho", @@ -83,6 +84,13 @@ "SignUpToCreateWorkspace": "Não há espaços de trabalho com acesso de convidado. Por favor, cadastre-se para criar o seu.", "WhatIsYourName": "Qual é o seu nome?", "AccessExpired": "O tempo de acesso expirou. Por favor, solicite um novo link de acesso.", - "AccessNotActive": "O tempo de acesso ainda não começou. Começa em: " + "AccessNotActive": "O tempo de acesso ainda não começou. Começa em: ", + "JoinWithThisAccount": "Entrar com esta conta", + "UseDifferentAccount": "Usar outra conta", + "UseCurrentAccountToJoin": "Já está conectado?", + "LogInAndJoin": "Entrar e participar", + "SignUpAndJoin": "Cadastrar e participar", + "CreateNewAccount": "Criar nova conta", + "SignedInAs": "Conectado como {name}" } } diff --git a/plugins/login-assets/lang/pt.json b/plugins/login-assets/lang/pt.json index a997db0191..3426417091 100644 --- a/plugins/login-assets/lang/pt.json +++ b/plugins/login-assets/lang/pt.json @@ -20,6 +20,7 @@ "LastName": "Apelido", "FirstName": "Nome", "Join": "Participar", + "JoinWorkspace": "Participar em {workspaceName}", "Email": "Email", "Password": "Palavra-Passe", "Workspace": "Nome do espaço de trabalho", @@ -83,6 +84,13 @@ "SignUpToCreateWorkspace": "Não há espaços de trabalho com acesso de convidado. Por favor, cadastre-se para criar o seu.", "WhatIsYourName": "Qual é o seu nome?", "AccessExpired": "O tempo de acesso expirou. Por favor, solicite um novo link de acesso.", - "AccessNotActive": "O tempo de acesso ainda não começou. Começa em: " + "AccessNotActive": "O tempo de acesso ainda não começou. Começa em: ", + "JoinWithThisAccount": "Participar com esta conta", + "UseDifferentAccount": "Usar outra conta", + "UseCurrentAccountToJoin": "Já tem sessão iniciada?", + "LogInAndJoin": "Entrar e participar", + "SignUpAndJoin": "Registar e participar", + "CreateNewAccount": "Criar nova conta", + "SignedInAs": "Sessão iniciada como {name}" } } diff --git a/plugins/login-assets/lang/ru.json b/plugins/login-assets/lang/ru.json index 951693ca1b..43ea908368 100644 --- a/plugins/login-assets/lang/ru.json +++ b/plugins/login-assets/lang/ru.json @@ -20,6 +20,7 @@ "LastName": "Фамилия", "FirstName": "Имя", "Join": "Присоединиться", + "JoinWorkspace": "Присоединиться к {workspaceName}", "Email": "Email", "Password": "Пароль", "Workspace": "Рабочее пространство", @@ -83,6 +84,13 @@ "SignUpToCreateWorkspace": "Нет рабочих пространств с гостевым доступом. Пожалуйста, зарегистрируйтесь, чтобы создать своё.", "WhatIsYourName": "Как вас зовут?", "AccessExpired": "Время доступа истекло. Пожалуйста, запросите новую ссылку доступа.", - "AccessNotActive": "Время доступа еще не началось. Начинается через: " + "AccessNotActive": "Время доступа еще не началось. Начинается через: ", + "JoinWithThisAccount": "Присоединиться с этим аккаунтом", + "UseDifferentAccount": "Использовать другой аккаунт", + "UseCurrentAccountToJoin": "Уже вошли в аккаунт?", + "LogInAndJoin": "Войти и присоединиться", + "SignUpAndJoin": "Зарегистрироваться и присоединиться", + "CreateNewAccount": "Создать новый аккаунт", + "SignedInAs": "Вы вошли как {name}" } } diff --git a/plugins/login-assets/lang/tr.json b/plugins/login-assets/lang/tr.json index 56b3b1cec5..63e5f6610e 100644 --- a/plugins/login-assets/lang/tr.json +++ b/plugins/login-assets/lang/tr.json @@ -20,6 +20,7 @@ "LastName": "Soyad", "FirstName": "Ad", "Join": "Katıl", + "JoinWorkspace": "{workspaceName} alanına katıl", "Email": "E-posta", "Password": "Şifre", "Workspace": "Çalışma alanı adı", @@ -83,6 +84,13 @@ "SignUpToCreateWorkspace": "Misafir erişimi olan çalışma alanı yok. Kendi alanınızı oluşturmak için lütfen kayıt olun.", "WhatIsYourName": "Adınız nedir?", "AccessExpired": "Erişim süresi doldu. Lütfen yeni bir erişim bağlantısı isteyin.", - "AccessNotActive": "Erişim süresi henüz başlamadı. Başlangıç zamanı: " + "AccessNotActive": "Erişim süresi henüz başlamadı. Başlangıç zamanı: ", + "JoinWithThisAccount": "Bu hesapla katıl", + "UseDifferentAccount": "Farklı hesap kullan", + "UseCurrentAccountToJoin": "Zaten giriş yaptınız mı?", + "LogInAndJoin": "Giriş yap ve katıl", + "SignUpAndJoin": "Kayıt ol ve katıl", + "CreateNewAccount": "Yeni hesap oluştur", + "SignedInAs": "{name} olarak giriş yapıldı" } } diff --git a/plugins/login-assets/lang/zh.json b/plugins/login-assets/lang/zh.json index 68247e29e2..bac4d5e95a 100644 --- a/plugins/login-assets/lang/zh.json +++ b/plugins/login-assets/lang/zh.json @@ -20,6 +20,7 @@ "LastName": "姓", "FirstName": "名", "Join": "加入", + "JoinWorkspace": "加入 {workspaceName}", "Email": "电子邮件", "Password": "密码", "Workspace": "工作区名称", @@ -83,6 +84,13 @@ "SignUpToCreateWorkspace": "没有支持访客访问的工作区。请注册以创建您自己的工作区。", "WhatIsYourName": "您的姓名是什么?", "AccessExpired": "访问时间已过期。请申请新的访问链接。", - "AccessNotActive": "访问时间尚未开始。开始时间:" + "AccessNotActive": "访问时间尚未开始。开始时间:", + "JoinWithThisAccount": "使用此账户加入", + "UseDifferentAccount": "使用其他账户", + "UseCurrentAccountToJoin": "已登录?", + "LogInAndJoin": "登录并加入", + "SignUpAndJoin": "注册并加入", + "CreateNewAccount": "创建新账户", + "SignedInAs": "已以 {name} 身份登录" } } diff --git a/plugins/login-resources/src/components/Form.svelte b/plugins/login-resources/src/components/Form.svelte index 3ba6096c02..dfe8a86122 100644 --- a/plugins/login-resources/src/components/Form.svelte +++ b/plugins/login-resources/src/components/Form.svelte @@ -34,6 +34,7 @@ } export let caption: IntlString + export let captionParams: Record = {} export let status: Status export let fields: Field[] export let action: Action @@ -46,6 +47,8 @@ export let subtitle: string | undefined = undefined export let signUpDisabled = false export let isLoading: boolean = false + export let actionButtonDataId: string | undefined = undefined + export let secondaryButtonDataId: string | undefined = undefined const validate = makeSequential(async function validateAsync (language: string): Promise { if (ignoreInitialValidation || isLoading) return true @@ -147,7 +150,7 @@ {/if}
-
+
{/if} @@ -174,6 +177,7 @@
+ + +{:else} + +{/if} + + diff --git a/plugins/login-resources/src/plugin.ts b/plugins/login-resources/src/plugin.ts index abc687ea8a..54872176f4 100644 --- a/plugins/login-resources/src/plugin.ts +++ b/plugins/login-resources/src/plugin.ts @@ -34,6 +34,7 @@ export default mergeIds(loginId, login, { HaveAccount: '' as IntlString, LoadingAccount: '' as IntlString, Join: '' as IntlString, + JoinWorkspace: '' as IntlString<{ workspaceName: string }>, Workspace: '' as IntlString, SignUp: '' as IntlString, DoNotHaveAnAccount: '' as IntlString, @@ -75,6 +76,13 @@ export default mergeIds(loginId, login, { SignUpToCreateWorkspace: '' as IntlString, WhatIsYourName: '' as IntlString, AccessExpired: '' as IntlString, - AccessNotActive: '' as IntlString + AccessNotActive: '' as IntlString, + JoinWithThisAccount: '' as IntlString, + UseDifferentAccount: '' as IntlString, + UseCurrentAccountToJoin: '' as IntlString, + LogInAndJoin: '' as IntlString, + SignUpAndJoin: '' as IntlString, + CreateNewAccount: '' as IntlString, + SignedInAs: '' as IntlString<{ name: string }> } }) diff --git a/plugins/login-resources/src/utils.ts b/plugins/login-resources/src/utils.ts index 1b4252b104..fbadd4615b 100644 --- a/plugins/login-resources/src/utils.ts +++ b/plugins/login-resources/src/utils.ts @@ -558,12 +558,9 @@ export function navigateToWorkspace ( export async function checkJoined (inviteId: string): Promise { const token = getMetadata(presentation.metadata.Token) - if (token == null) return - try { - const workspaceLoginInfo = await getAccountClient(token).checkJoin(inviteId) - - return workspaceLoginInfo + const workspaceLoginInfo = await getAccountClient(token ?? undefined).checkJoin(inviteId) + return workspaceLoginInfo ?? undefined } catch (err: any) { if (!(err instanceof PlatformError)) { Analytics.handleError(err) @@ -571,6 +568,29 @@ export async function checkJoined (inviteId: string): Promise { + try { + const client = getAccountClient(null) + const info = await client.getInviteInfo(inviteId) + return info.workspaceName ?? undefined + } catch (err: any) { + console.error('Failed to get invite workspace name', err) + if (!(err instanceof PlatformError)) { + Analytics.handleError(err) + } + return undefined + } +} + +export async function joinByToken (inviteId: string): Promise { + const token = getMetadata(presentation.metadata.Token) + + return await getAccountClient(token ?? undefined).joinByToken(inviteId) +} + export async function checkAutoJoin ( inviteId: string, firstName?: string, diff --git a/server/account/src/operations.ts b/server/account/src/operations.ts index c8f0ccf0eb..ab55e766ec 100644 --- a/server/account/src/operations.ts +++ b/server/account/src/operations.ts @@ -64,7 +64,8 @@ import { type PersonWithProfile, type Subscription, SubscriptionStatus, - type Query + type Query, + type InviteInfo } from './types' import { addSocialIdBase, @@ -993,6 +994,38 @@ export async function join ( return await doJoinByInvite(ctx, db, branding, token, account, workspaceJoinInfo.workspace, workspaceJoinInfo.invite) } +/** + * Returns public invite details (e.g. workspace name) for a valid invite. No auth required. + * Returns { workspaceName: null } for invalid or expired invites. + */ +export async function getInviteInfo ( + ctx: MeasureContext, + db: AccountDB, + _branding: Branding | null, + _token: string, + params: { inviteId: string } +): Promise { + const { inviteId } = params + + if (inviteId == null || inviteId === '') { + ctx.error('Mandatory param inviteId is missing in getInviteInfo', { inviteId }) + return { workspaceName: null } + } + + const invite = await getWorkspaceInvite(db, inviteId) + if (invite == null) { + ctx.error('Invite not found in getInviteInfo', { inviteId }) + return { workspaceName: null } + } + + const workspace = await getWorkspaceById(db, invite.workspaceUuid) + if (workspace === null) { + return { workspaceName: null } + } + + return { workspaceName: workspace.name } +} + /** * Given an invite and a token, checks if the user has already joined the workspace and updates the role if necessary. * Returns the workspace login information if the user has already joined. Otherwise, throws an error. @@ -1042,6 +1075,46 @@ export async function checkJoin ( } } +/** + * Joins the workspace using the current session token (no password). + * Called only when the user explicitly clicks "Join with this account". + */ +export async function joinByToken ( + ctx: MeasureContext, + db: AccountDB, + branding: Branding | null, + token: string, + params: { inviteId: string } +): Promise { + const { inviteId } = params + + if (inviteId == null || inviteId === '') { + throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {})) + } + + const invite = await getWorkspaceInvite(db, inviteId) + if (invite == null) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) + } + + const { account: accountUuid } = decodeTokenVerbose(ctx, token) + const emailSocialId = await db.socialId.findOne({ + type: SocialIdType.EMAIL, + personUuid: accountUuid, + verifiedOn: { $gt: 0 } + }) + const email = emailSocialId?.value ?? '' + const workspaceUuid = await checkInvite(ctx, invite, email) + const workspace = await getWorkspaceById(db, workspaceUuid) + + if (workspace === null) { + ctx.error('Workspace not found in joinByToken', { workspaceUuid, email, inviteId }) + throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUuid })) + } + + return await doJoinByInvite(ctx, db, branding, token, accountUuid, workspace, invite) +} + export async function checkAutoJoin ( ctx: MeasureContext, db: AccountDB, @@ -2892,8 +2965,10 @@ export type AccountMethods = | 'resendInvite' | 'selectWorkspace' | 'join' + | 'joinByToken' | 'checkJoin' | 'checkAutoJoin' + | 'getInviteInfo' | 'signUpJoin' | 'confirm' | 'changePassword' @@ -2966,8 +3041,10 @@ export function getMethods (hasSignUp: boolean = true): Partial this.page.locator('input[name="email"]') inputPassword = (): Locator => this.page.locator('//div[text()="Password"]/../input') - buttonJoin = (): Locator => this.page.locator('button', { hasText: 'Join' }) + buttonSubmit = (): Locator => this.page.locator('button[data-id="join-form-submit"]') + buttonToggle = (): Locator => this.page.locator('button[data-id="join-form-toggle"]') + buttonJoinWithThisAccount = (): Locator => this.page.locator('button[data-id="join-with-this-account"]') + buttonUseDifferentAccount = (): Locator => this.page.locator('button[data-id="join-use-different-account"]') async join (data: Pick): Promise { - await this.buttonJoin().click() + await Promise.race([ + this.buttonSubmit().waitFor({ state: 'visible', timeout: 10000 }), + this.buttonJoinWithThisAccount().waitFor({ state: 'visible', timeout: 10000 }) + ]) + + const submitBtn = this.buttonSubmit() + const toggleBtn = this.buttonToggle() + const joinWithAccountBtn = this.buttonJoinWithThisAccount() + + if (await joinWithAccountBtn.isVisible({ timeout: 2000 }).catch(() => false)) { + await this.buttonUseDifferentAccount().click() + await this.inputEmail().waitFor({ state: 'visible', timeout: 5000 }) + } + + const submitText = await submitBtn.textContent().catch(() => '') + if (submitText != null && submitText.toLowerCase().includes('sign up')) { + await toggleBtn.click() + await this.inputEmail().waitFor({ state: 'visible', timeout: 5000 }) + } await this.inputEmail().fill(data.email) await this.inputPassword().fill(data.password) - expect(await this.buttonJoin().isEnabled()).toBe(true) - await this.buttonJoin().click() + await expect(this.buttonSubmit()).toBeEnabled() + await this.buttonSubmit().click() } }