From 5bef324c7d5e11a4bb80102095410ae483f9aa8a Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Wed, 24 Jul 2024 18:51:57 +0200 Subject: [PATCH 01/55] progress --- .../src/components/ui/icons/ChangeIcon.tsx | 30 +++ .../src/components/ui/icons/CopyIcon.tsx | 30 +++ .../react/src/screens/core/PasskeyList.tsx | 4 +- packages/react/src/screens/core/User.tsx | 231 +++++++++++------- packages/shared-ui/src/assets/change.svg | 1 + packages/shared-ui/src/assets/copy.svg | 1 + packages/shared-ui/src/i18n/en.json | 6 +- packages/shared-ui/src/index.ts | 4 + .../shared-ui/src/styles/passkey-list.css | 6 +- packages/shared-ui/src/styles/user.css | 77 +++++- 10 files changed, 289 insertions(+), 101 deletions(-) create mode 100644 packages/react/src/components/ui/icons/ChangeIcon.tsx create mode 100644 packages/react/src/components/ui/icons/CopyIcon.tsx create mode 100644 packages/shared-ui/src/assets/change.svg create mode 100644 packages/shared-ui/src/assets/copy.svg diff --git a/packages/react/src/components/ui/icons/ChangeIcon.tsx b/packages/react/src/components/ui/icons/ChangeIcon.tsx new file mode 100644 index 000000000..167d492c9 --- /dev/null +++ b/packages/react/src/components/ui/icons/ChangeIcon.tsx @@ -0,0 +1,30 @@ +import changeIconSrc from '@corbado/shared-ui/assets/change.svg'; +import type { FC } from 'react'; +import { useRef } from 'react'; +import React from 'react'; + +import { useIconWithTheme } from '../../../hooks/useIconWithTheme'; +import type { IconProps } from './Icon'; +import { Icon } from './Icon'; + +export interface ChangeIconProps extends IconProps { + color?: 'primary' | 'secondary'; +} + +export const ChangeIcon: FC = ({ color, ...props }) => { + const svgRef = useRef(null); + const { logoSVG } = useIconWithTheme( + svgRef, + changeIconSrc, + color === 'secondary' ? '--cb-text-secondary-color' : '--cb-text-primary-color', + ); + + return ( + + ); +}; diff --git a/packages/react/src/components/ui/icons/CopyIcon.tsx b/packages/react/src/components/ui/icons/CopyIcon.tsx new file mode 100644 index 000000000..a9d7cd14e --- /dev/null +++ b/packages/react/src/components/ui/icons/CopyIcon.tsx @@ -0,0 +1,30 @@ +import copyIconSrc from '@corbado/shared-ui/assets/copy.svg'; +import type { FC } from 'react'; +import { useRef } from 'react'; +import React from 'react'; + +import { useIconWithTheme } from '../../../hooks/useIconWithTheme'; +import type { IconProps } from './Icon'; +import { Icon } from './Icon'; + +export interface CopyIconProps extends IconProps { + color?: 'primary' | 'secondary'; +} + +export const CopyIcon: FC = ({ color, ...props }) => { + const svgRef = useRef(null); + const { logoSVG } = useIconWithTheme( + svgRef, + copyIconSrc, + color === 'secondary' ? '--cb-text-secondary-color' : '--cb-text-primary-color', + ); + + return ( + + ); +}; diff --git a/packages/react/src/screens/core/PasskeyList.tsx b/packages/react/src/screens/core/PasskeyList.tsx index d4381a525..995df232e 100644 --- a/packages/react/src/screens/core/PasskeyList.tsx +++ b/packages/react/src/screens/core/PasskeyList.tsx @@ -53,9 +53,7 @@ const PasskeyList: FC = () => { return (
-
- {t('title')} -
+ {t('title')} {passkeys?.passkeys.map(passkey => ( { }; }, [isAuthenticated]); - const headerText = useMemo(() => t('header'), [t]); const nameFieldLabel = useMemo(() => t('name'), [t]); const usernameFieldLabel = useMemo(() => t('username'), [t]); const emailFieldLabel = useMemo(() => t('email'), [t]); @@ -78,6 +79,22 @@ export const User: FC = () => { [corbadoApp], ); + const copyName = async () => { + await navigator.clipboard.writeText(processUser.name); + }; + + const changeName = () => { + return; + }; + + const copyUsername = async () => { + await navigator.clipboard.writeText(processUser.username || ''); + }; + + const changeUsername = () => { + return; + }; + if (!isAuthenticated) { return
{t('warning_notLoggedIn')}
; } @@ -88,120 +105,152 @@ export const User: FC = () => { return ( -
- - {headerText} - +
+ {t('title')} {processUser.name && ( - +
+ {nameFieldLabel} +
+
+ + void copyName()} + /> +
+ +
+
)} {processUser.username && ( - - )} -
- {processUser.emails.map((email, i) => ( -
-
+
+ {usernameFieldLabel} +
+
+ void copyUsername()} + />
+ +
+
+ )} +
+ {processUser.emails.map((email, i) => ( +
- + +
+
- {email.status === 'verified' ? verifiedText : unverifiedText} - + + {email.status === 'verified' ? verifiedText : unverifiedText} + +
))}
{processUser.phoneNumbers.map((phone, i) => ( -
-
- -
+
- + +
+
- {phone.status === 'verified' ? verifiedText : unverifiedText} - + + {phone.status === 'verified' ? verifiedText : unverifiedText} + +
))}
{processUser.socialAccounts.map((social, i) => ( -
-
- -
-
- - {t(`providers.${social.providerType}`) || social.providerType} - +
+
+
+ +
+
+ + {t(`providers.${social.providerType}`) || social.providerType} + +
))} diff --git a/packages/shared-ui/src/assets/change.svg b/packages/shared-ui/src/assets/change.svg new file mode 100644 index 000000000..b5a4dead0 --- /dev/null +++ b/packages/shared-ui/src/assets/change.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/shared-ui/src/assets/copy.svg b/packages/shared-ui/src/assets/copy.svg new file mode 100644 index 000000000..d5784eefc --- /dev/null +++ b/packages/shared-ui/src/assets/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/shared-ui/src/i18n/en.json b/packages/shared-ui/src/i18n/en.json index febc8af41..b017b405a 100644 --- a/packages/shared-ui/src/i18n/en.json +++ b/packages/shared-ui/src/i18n/en.json @@ -379,7 +379,7 @@ } }, "passkey-list": { - "title": "PASSKEYS", + "title": "Passkeys", "warning_notLoggedIn": "Please log in to see your passkeys.", "message_noPasskeys": "You don't have any passkeys yet.", "button_createPasskey": "Create a Passkey", @@ -408,10 +408,10 @@ } }, "user": { - "header": "Your Account", + "title": "User Details", "name": "Name", "username": "Username", - "email": "Email", + "email": "Email address", "phone": "Phone number", "social": "Social accounts", "verified": "verified", diff --git a/packages/shared-ui/src/index.ts b/packages/shared-ui/src/index.ts index e2d13daf8..84fb94f30 100644 --- a/packages/shared-ui/src/index.ts +++ b/packages/shared-ui/src/index.ts @@ -3,7 +3,9 @@ import './styles/index.css'; import addIcon from './assets/add.svg'; import appleIcon from './assets/apple.svg'; import cancelIcon from './assets/cancel.svg'; +import changeIcon from './assets/change.svg'; import circleExclamationIcon from './assets/circle-exclamation.svg'; +import copyIcon from './assets/copy.svg'; import deleteIcon from './assets/delete.svg'; import deviceIcon from './assets/device-icon.svg'; import editIcon from './assets/edit.svg'; @@ -96,4 +98,6 @@ export const assets = { passkeyHybridIcon, passkeyHybridDarkIcon, lockIcon, + copyIcon, + changeIcon, }; diff --git a/packages/shared-ui/src/styles/passkey-list.css b/packages/shared-ui/src/styles/passkey-list.css index 7875f9cf0..8f447753a 100644 --- a/packages/shared-ui/src/styles/passkey-list.css +++ b/packages/shared-ui/src/styles/passkey-list.css @@ -6,8 +6,10 @@ } .cb-passkey-list-title { - font-size: calc(var(--cb-base-font-size) * 1.9); - margin: 0.5rem 0rem 2rem 0rem; + font-size: calc(var(--cb-base-font-size) * 2.7); + font-family: var(--cb-primary-font); + font-weight: bold; + margin: 0rem 0rem 0.5rem 1rem; } .cb-passkey-list-card { diff --git a/packages/shared-ui/src/styles/user.css b/packages/shared-ui/src/styles/user.css index c791c21b6..16254e102 100644 --- a/packages/shared-ui/src/styles/user.css +++ b/packages/shared-ui/src/styles/user.css @@ -12,14 +12,87 @@ margin-bottom: 1rem; } +.cb-user-details-title { + font-size: calc(var(--cb-base-font-size) * 2.7); + font-family: var(--cb-primary-font); + font-weight: bold; + margin: 0.5rem 1rem; +} + +.cb-user-details-card { + /* display: flex; + flex-direction: row; */ + gap: 1rem; + border-radius: var(--cb-border-radius-sm); + border: var(--cb-passkey-list-border-color) 1px solid; + margin: 0.5rem 0; + padding: 1rem 1rem; + background-color: var(--cb-white); + line-height: 1.4; + overflow: hidden; +} + +.cb-user-details-card:hover { + border: var(--cb-passkey-list-border-hover-color) 1px solid; +} + +.cb-user-details-header { + margin: auto 0; + margin-bottom: 0.5rem; + font-size: calc(var(--cb-base-font-size) * 1.9); + font-family: var(--cb-primary-font); + font-weight: bold; + float: left; +} + +.cb-user-details-subheader { + font-size: calc(var(--cb-base-font-size) * 1.4); + font-family: var(--cb-primary-font); + font-weight: bold; +} + +.cb-user-details-body { + width: 100%; + max-width: 500px; + float: right; + clear: right; +} + +.cb-user-details-body-row { + display: flex; + flex-direction: row; + align-items: center; +} + +.cb-user-details-body-row-icon { + height: 1.5rem; + cursor: pointer; + margin-left: 0.5rem; + margin-bottom: 0.75rem; +} + +.cb-user-details-body-button { + padding: 0.4rem 2rem; + border: 1px solid #ccc; + border-radius: 0.5rem; + background-color: white; + cursor: pointer; + box-sizing: border-box; + font-family: var(--cb-font-family-primary); +} + +.cb-user-details-body-button-icon { + height: 1rem; +} + .cb-user-details-section-indentifier { width: 85%; align-self: self-start; } -.cb-user-details-section-indentifiers-list { +/* .cb-user-details-section-indentifiers-list { margin-top: 1rem; -} +} */ .cb-user-details-section-indentifiers-list-item { display: flex; From 5c86637ce637b7cb383dff9af04e6783ef594fa3 Mon Sep 17 00:00:00 2001 From: aehnh Date: Mon, 29 Jul 2024 18:35:09 +0200 Subject: [PATCH 02/55] modify fullname and username --- .../src/contexts/CorbadoSessionContext.tsx | 4 + .../src/contexts/CorbadoSessionProvider.tsx | 16 +++ packages/react/src/screens/core/User.tsx | 98 +++++++++++++------ packages/shared-ui/src/styles/user.css | 37 ++++++- packages/types/src/session.ts | 2 + packages/web-core/src/api/v2/base.ts | 2 +- .../web-core/src/services/SessionService.ts | 20 +++- packages/web-js/src/core/Corbado.ts | 8 ++ 8 files changed, 151 insertions(+), 36 deletions(-) diff --git a/packages/react/src/contexts/CorbadoSessionContext.tsx b/packages/react/src/contexts/CorbadoSessionContext.tsx index 7b3647b6b..6891c894d 100644 --- a/packages/react/src/contexts/CorbadoSessionContext.tsx +++ b/packages/react/src/contexts/CorbadoSessionContext.tsx @@ -18,6 +18,8 @@ export interface CorbadoSessionContextProps { getPasskeys: (abortController?: AbortController) => Promise>; deletePasskey: (id: string) => Promise>; getFullUser: (abortController?: AbortController) => Promise>; + updateName: (fullName: string) => Promise>; + updateUsername: (identifierID: string, username: string) => Promise>; globalError: NonRecoverableError | undefined; } @@ -33,6 +35,8 @@ export const initialContext: CorbadoSessionContextProps = { getPasskeys: missingImplementation, deletePasskey: missingImplementation, getFullUser: missingImplementation, + updateName: missingImplementation, + updateUsername: missingImplementation, }; export const CorbadoSessionContext = createContext(initialContext); diff --git a/packages/react/src/contexts/CorbadoSessionProvider.tsx b/packages/react/src/contexts/CorbadoSessionProvider.tsx index 48ca99058..0c7d76a21 100644 --- a/packages/react/src/contexts/CorbadoSessionProvider.tsx +++ b/packages/react/src/contexts/CorbadoSessionProvider.tsx @@ -86,6 +86,20 @@ export const CorbadoSessionProvider: FC = ({ [corbadoApp], ); + const updateName = useCallback( + (fullName: string) => { + return corbadoApp.sessionService.updateName(fullName); + }, + [corbadoApp], + ); + + const updateUsername = useCallback( + (identifierID: string, username: string) => { + return corbadoApp.sessionService.updateUsername(identifierID, username); + }, + [corbadoApp], + ); + return ( = ({ isAuthenticated, appendPasskey, getFullUser, + updateName, + updateUsername, getPasskeys, deletePasskey, logout, diff --git a/packages/react/src/screens/core/User.tsx b/packages/react/src/screens/core/User.tsx index 5eebc9a8e..59b21b4f6 100644 --- a/packages/react/src/screens/core/User.tsx +++ b/packages/react/src/screens/core/User.tsx @@ -1,4 +1,4 @@ -import type { CorbadoUser, Identifier, SocialAccount } from '@corbado/types'; +import { LoginIdentifierType, type CorbadoUser, type Identifier, type SocialAccount } from '@corbado/types'; import type { FC } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -17,10 +17,16 @@ interface ProcessedUser { } export const User: FC = () => { - const { corbadoApp, isAuthenticated, globalError, getFullUser } = useCorbado(); + const { corbadoApp, isAuthenticated, globalError, getFullUser, updateName, updateUsername } = useCorbado(); const { t } = useTranslation('translation', { keyPrefix: 'user' }); const [currentUser, setCurrentUser] = useState(); const [loading, setLoading] = useState(false); + const [editingName, setEditingName] = useState(false); + const [editingUsername, setEditingUsername] = useState(false); + const [name, setName] = useState(""); + const [username, setUsername] = useState(""); + + let usernameIdentifierID = ""; useEffect(() => { if (!isAuthenticated) { @@ -62,7 +68,7 @@ export const User: FC = () => { }, [currentUser]); const getCurrentUser = useCallback( - async (abortController: AbortController) => { + async (abortController?: AbortController) => { setLoading(true); const result = await getFullUser(abortController); if (result.err && result.val.ignore) { @@ -74,27 +80,23 @@ export const User: FC = () => { } setCurrentUser(result.val); + setName(result.val.fullName || ""); + let usernameIdentifier = result.val.identifiers.find(identifier => identifier.type == LoginIdentifierType.Username); + setUsername(usernameIdentifier?.value || ""); + usernameIdentifierID = usernameIdentifier?.id || ""; setLoading(false); }, [corbadoApp], ); const copyName = async () => { - await navigator.clipboard.writeText(processUser.name); - }; - - const changeName = () => { - return; + await navigator.clipboard.writeText(name); }; const copyUsername = async () => { await navigator.clipboard.writeText(processUser.username || ''); }; - const changeUsername = () => { - return; - }; - if (!isAuthenticated) { return
{t('warning_notLoggedIn')}
; } @@ -105,17 +107,18 @@ export const User: FC = () => { return ( -
+
{t('title')} - {processUser.name && ( + {name !== "" && (
{nameFieldLabel}
setName(e.target.value)} /> { onClick={() => void copyName()} />
- + {editingName ? ( +
+ + +
+ ) : ( + + )}
)} - {processUser.username && ( + {username !== "" && (
{usernameFieldLabel}
setName(e.target.value)} /> { onClick={() => void copyUsername()} />
- + {editingUsername ? ( +
+ + +
+ ) : ( + + )}
)} diff --git a/packages/shared-ui/src/styles/user.css b/packages/shared-ui/src/styles/user.css index 16254e102..34c759baa 100644 --- a/packages/shared-ui/src/styles/user.css +++ b/packages/shared-ui/src/styles/user.css @@ -1,3 +1,10 @@ +.cb-user-details-container { + display: flex; + flex-direction: column; + margin: 1.25rem 0rem; + text-align: left; +} + .cb-user-details-section { display: flex; flex-direction: column; @@ -46,7 +53,7 @@ } .cb-user-details-subheader { - font-size: calc(var(--cb-base-font-size) * 1.4); + font-size: calc(var(--cb-base-font-size) * 1.5); font-family: var(--cb-primary-font); font-weight: bold; } @@ -72,6 +79,8 @@ } .cb-user-details-body-button { + display: flex; + align-items: center; padding: 0.4rem 2rem; border: 1px solid #ccc; border-radius: 0.5rem; @@ -82,7 +91,31 @@ } .cb-user-details-body-button-icon { - height: 1rem; + height: 1em; + width: auto; + margin-right: 8px; +} + +.cb-user-details-body-button-primary { + padding: 0.4rem 2rem; + color: var(--cb-color-on-primary); + background-color: var(--cb-color-primary); + border: 1px solid transparent; + border-radius: var(--cb-border-radius); + cursor: pointer; + box-sizing: border-box; + font-family: var(--cb-font-family-primary); +} + +.cb-user-details-body-button-secondary { + padding: 0.4rem 2rem; + color: var(--cb-color-on-secondary); + background-color: var(--cb-color-secondary); + border: 1px solid transparent; + border-radius: var(--cb-border-radius); + cursor: pointer; + box-sizing: border-box; + font-family: var(--cb-font-family-primary); } .cb-user-details-section-indentifier { diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index e5119ba9e..baebebb51 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -43,11 +43,13 @@ export interface CorbadoUser { /** * Interface for Identifier. * @interface + * @property {string} id - The ID of the identifier * @property {string} value - The value of the identifier. * @property {LoginIdentifierType} type - The type of the identifier. * @property {string} status - The status of the identifier. */ export interface Identifier { + id: string; value: string; type: LoginIdentifierType; status: string; diff --git a/packages/web-core/src/api/v2/base.ts b/packages/web-core/src/api/v2/base.ts index a378b2b3e..774913341 100644 --- a/packages/web-core/src/api/v2/base.ts +++ b/packages/web-core/src/api/v2/base.ts @@ -19,7 +19,7 @@ import type { Configuration } from './configuration'; import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; import globalAxios from 'axios'; -export const BASE_PATH = "https://.frontendapi.corbado.io".replace(/\/+$/, ""); +export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); /** * diff --git a/packages/web-core/src/services/SessionService.ts b/packages/web-core/src/services/SessionService.ts index 40dbee359..370b73b87 100644 --- a/packages/web-core/src/services/SessionService.ts +++ b/packages/web-core/src/services/SessionService.ts @@ -15,7 +15,7 @@ import { Err, Ok, Result } from 'ts-results'; import { Configuration } from '../api/v1'; import type { SessionConfigRsp, ShortSessionCookieConfig } from '../api/v2'; -import { ConfigsApi, UsersApi } from '../api/v2'; +import { ConfigsApi, LoginIdentifierType, UsersApi } from '../api/v2'; import { ShortSession } from '../models/session'; import { AuthState, @@ -157,6 +157,24 @@ export class SessionService { return this.wrapWithErr(async () => this.#usersApi.currentUserGet({ signal: abortController.signal })); } + async updateName(fullName: string): Promise> { + return Result.wrapAsync(async () => { + await this.#usersApi.currentUserUpdate({ fullName }); + return void 0; + }); + } + + async updateUsername(identifierID: string, username: string): Promise> { + return Result.wrapAsync(async () => { + await this.#usersApi.currentUserIdentifierUpdate({ + identifierID, + identifierType: LoginIdentifierType.Email, + value: username, + }); + return void 0; + }) + } + async appendPasskey(): Promise> { const canUsePasskeys = await WebAuthnService.doesBrowserSupportPasskeys(); diff --git a/packages/web-js/src/core/Corbado.ts b/packages/web-js/src/core/Corbado.ts index a2113abdc..8c94877d7 100644 --- a/packages/web-js/src/core/Corbado.ts +++ b/packages/web-js/src/core/Corbado.ts @@ -106,6 +106,14 @@ export class Corbado { return this.#getCorbadoAppState().corbadoApp.sessionService.getFullUser(abortController ?? new AbortController()); } + updateName(fullName: string) { + return this.#getCorbadoAppState().corbadoApp.sessionService.updateName(fullName); + } + + updateUsername(identifierID: string, username: string) { + return this.#getCorbadoAppState().corbadoApp.sessionService.updateUsername(identifierID, username); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any #mountComponent = >(element: HTMLElement, Component: FC, componentOptions: T) => { if (!this.#corbadoAppState) { From 6b14c3a6846cfe79aecc6bb115a951c50336cfc5 Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Tue, 30 Jul 2024 10:54:32 +0200 Subject: [PATCH 03/55] revert corbado/types --- packages/react/src/screens/core/User.tsx | 22 +++++++++++++++++----- packages/types/src/session.ts | 2 -- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/react/src/screens/core/User.tsx b/packages/react/src/screens/core/User.tsx index 59b21b4f6..dd1a2f840 100644 --- a/packages/react/src/screens/core/User.tsx +++ b/packages/react/src/screens/core/User.tsx @@ -1,4 +1,4 @@ -import { LoginIdentifierType, type CorbadoUser, type Identifier, type SocialAccount } from '@corbado/types'; +import { type CorbadoUser, type Identifier, LoginIdentifierType, type SocialAccount } from '@corbado/types'; import type { FC } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -81,9 +81,9 @@ export const User: FC = () => { setCurrentUser(result.val); setName(result.val.fullName || ""); - let usernameIdentifier = result.val.identifiers.find(identifier => identifier.type == LoginIdentifierType.Username); + const usernameIdentifier = result.val.identifiers.find(identifier => identifier.type == LoginIdentifierType.Username); setUsername(usernameIdentifier?.value || ""); - usernameIdentifierID = usernameIdentifier?.id || ""; + usernameIdentifierID = /*usernameIdentifier?.id ||*/ ""; setLoading(false); }, [corbadoApp], @@ -93,10 +93,22 @@ export const User: FC = () => { await navigator.clipboard.writeText(name); }; + const changeName = async () => { + await updateName(name); + setEditingName(false); + void getCurrentUser(); + }; + const copyUsername = async () => { await navigator.clipboard.writeText(processUser.username || ''); }; + const changeUsername = async () => { + await updateUsername(usernameIdentifierID, username); + setEditingUsername(false); + void getCurrentUser(); + } + if (!isAuthenticated) { return
{t('warning_notLoggedIn')}
; } @@ -130,7 +142,7 @@ export const User: FC = () => {
)} - {username !== "" && ( + {/* {username !== "" && (
{usernameFieldLabel}
@@ -183,7 +183,7 @@ export const User: FC = () => {
- )} + )} */}
{processUser.emails.map((email, i) => (
From 9450c86d96cc6898d6f34e99cb5d72a0e70494df Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Tue, 30 Jul 2024 19:48:01 +0200 Subject: [PATCH 07/55] udpate username works --- packages/react/src/screens/core/User.tsx | 49 ++++++++++--------- packages/types/src/session.ts | 2 + .../web-core/src/services/SessionService.ts | 2 +- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/packages/react/src/screens/core/User.tsx b/packages/react/src/screens/core/User.tsx index 8a9d57d8f..0dcb307ca 100644 --- a/packages/react/src/screens/core/User.tsx +++ b/packages/react/src/screens/core/User.tsx @@ -7,26 +7,26 @@ import { Button, InputField, LoadingSpinner, PasskeyListErrorBoundary, PhoneInpu import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; import { CopyIcon } from '../../components/ui/icons/CopyIcon'; import { useCorbado } from '../../hooks/useCorbado'; +import { LoginIdentifierType } from '@corbado/shared-ui'; interface ProcessedUser { name: string; - username?: string; + username: string; emails: Identifier[]; phoneNumbers: Identifier[]; socialAccounts: SocialAccount[]; } export const User: FC = () => { - const { corbadoApp, isAuthenticated, globalError, getFullUser, updateName, /*updateUsername*/ } = useCorbado(); + const { corbadoApp, isAuthenticated, globalError, getFullUser, updateName, updateUsername } = useCorbado(); const { t } = useTranslation('translation', { keyPrefix: 'user' }); const [currentUser, setCurrentUser] = useState(); const [loading, setLoading] = useState(false); const [editingName, setEditingName] = useState(false); - // const [editingUsername, setEditingUsername] = useState(false); + const [editingUsername, setEditingUsername] = useState(false); const [name, setName] = useState(""); - // const [username, setUsername] = useState(""); - - // let usernameIdentifierID = ""; + const [username, setUsername] = useState(""); + const [usernameIdentifierID, setUsernameIdentifierID] = useState(""); useEffect(() => { if (!isAuthenticated) { @@ -42,7 +42,7 @@ export const User: FC = () => { }, [isAuthenticated]); const nameFieldLabel = useMemo(() => t('name'), [t]); - // const usernameFieldLabel = useMemo(() => t('username'), [t]); + const usernameFieldLabel = useMemo(() => t('username'), [t]); const emailFieldLabel = useMemo(() => t('email'), [t]); const phoneFieldLabel = useMemo(() => t('phone'), [t]); const socialFieldLabel = useMemo(() => t('social'), [t]); @@ -52,6 +52,7 @@ export const User: FC = () => { if (!currentUser) { return { name: '', + username: '', emails: [], phoneNumbers: [], socialAccounts: [], @@ -60,7 +61,7 @@ export const User: FC = () => { return { name: currentUser.fullName, - username: currentUser.identifiers.find(id => id.type === 'username')?.value, + username: currentUser.identifiers.find(id => id.type === 'username')?.value || '', emails: currentUser.identifiers.filter(id => id.type === 'email'), phoneNumbers: currentUser.identifiers.filter(id => id.type === 'phone'), socialAccounts: currentUser.socialAccounts, @@ -81,9 +82,9 @@ export const User: FC = () => { setCurrentUser(result.val); setName(result.val.fullName || ""); - // const usernameIdentifier = result.val.identifiers.find(identifier => identifier.type == LoginIdentifierType.Username); - // setUsername(usernameIdentifier?.value || ""); - // usernameIdentifierID = /*usernameIdentifier?.id ||*/ ""; + const usernameIdentifier = result.val.identifiers.find(identifier => identifier.type == LoginIdentifierType.Username); + setUsername(usernameIdentifier?.value || ""); + setUsernameIdentifierID(usernameIdentifier?.id || ""); setLoading(false); }, [corbadoApp], @@ -99,15 +100,15 @@ export const User: FC = () => { void getCurrentUser(); }; - // const copyUsername = async () => { - // await navigator.clipboard.writeText(processUser.username || ''); - // }; + const copyUsername = async () => { + await navigator.clipboard.writeText(username); + }; - // const changeUsername = async () => { - // await updateUsername(usernameIdentifierID, username); - // setEditingUsername(false); - // void getCurrentUser(); - // } + const changeUsername = async () => { + await updateUsername(usernameIdentifierID, username); + setEditingUsername(false); + void getCurrentUser(); + } if (!isAuthenticated) { return
{t('warning_notLoggedIn')}
; @@ -162,16 +163,16 @@ export const User: FC = () => {
)} - {/* {username !== "" && ( + {username !== "" && (
{usernameFieldLabel}
setName(e.target.value)} + onChange={e => setUsername(e.target.value)} /> {
@@ -202,7 +203,7 @@ export const User: FC = () => { )}
- )} */} + )}
{processUser.emails.map((email, i) => (
diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index e5119ba9e..505cc6a33 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -43,11 +43,13 @@ export interface CorbadoUser { /** * Interface for Identifier. * @interface + * @property {string} id - The ID of the identifier. * @property {string} value - The value of the identifier. * @property {LoginIdentifierType} type - The type of the identifier. * @property {string} status - The status of the identifier. */ export interface Identifier { + id: string; value: string; type: LoginIdentifierType; status: string; diff --git a/packages/web-core/src/services/SessionService.ts b/packages/web-core/src/services/SessionService.ts index 370b73b87..4b7dffb06 100644 --- a/packages/web-core/src/services/SessionService.ts +++ b/packages/web-core/src/services/SessionService.ts @@ -168,7 +168,7 @@ export class SessionService { return Result.wrapAsync(async () => { await this.#usersApi.currentUserIdentifierUpdate({ identifierID, - identifierType: LoginIdentifierType.Email, + identifierType: LoginIdentifierType.Username, value: username, }); return void 0; From 30c63bda93d33625773ef61b657feaf6ccb97c9a Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Mon, 5 Aug 2024 13:03:44 +0200 Subject: [PATCH 08/55] email address component wip --- packages/react/src/screens/core/User.tsx | 81 ++++++++++++++++++++---- packages/shared-ui/src/styles/user.css | 10 +++ 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/packages/react/src/screens/core/User.tsx b/packages/react/src/screens/core/User.tsx index 0dcb307ca..7d759b725 100644 --- a/packages/react/src/screens/core/User.tsx +++ b/packages/react/src/screens/core/User.tsx @@ -8,6 +8,7 @@ import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; import { CopyIcon } from '../../components/ui/icons/CopyIcon'; import { useCorbado } from '../../hooks/useCorbado'; import { LoginIdentifierType } from '@corbado/shared-ui'; +import { AddIcon } from '../../components/ui/icons/AddIcon'; interface ProcessedUser { name: string; @@ -22,11 +23,17 @@ export const User: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'user' }); const [currentUser, setCurrentUser] = useState(); const [loading, setLoading] = useState(false); + + const [name, setName] = useState(""); const [editingName, setEditingName] = useState(false); + + const [username, setUsername] = useState(); const [editingUsername, setEditingUsername] = useState(false); - const [name, setName] = useState(""); - const [username, setUsername] = useState(""); - const [usernameIdentifierID, setUsernameIdentifierID] = useState(""); + + const [emails, setEmails] = useState([]); + const [addingEmail, setAddingEmail] = useState(false); + const [newEmail, setNewEmail] = useState(""); + useEffect(() => { if (!isAuthenticated) { @@ -83,8 +90,8 @@ export const User: FC = () => { setCurrentUser(result.val); setName(result.val.fullName || ""); const usernameIdentifier = result.val.identifiers.find(identifier => identifier.type == LoginIdentifierType.Username); - setUsername(usernameIdentifier?.value || ""); - setUsernameIdentifierID(usernameIdentifier?.id || ""); + setUsername(usernameIdentifier); + setEmails(result.val.identifiers.filter(identifier => identifier.type == LoginIdentifierType.Email)); setLoading(false); }, [corbadoApp], @@ -95,20 +102,28 @@ export const User: FC = () => { }; const changeName = async () => { + // TODO: input checking? await updateName(name); setEditingName(false); void getCurrentUser(); }; const copyUsername = async () => { - await navigator.clipboard.writeText(username); + await navigator.clipboard.writeText(username?.value || ""); }; const changeUsername = async () => { - await updateUsername(usernameIdentifierID, username); + // TODO: input checking? + if (username) { + await updateUsername(username.id, username.value); + } setEditingUsername(false); void getCurrentUser(); - } + }; + + const addEmail = async () => { + return; + }; if (!isAuthenticated) { return
{t('warning_notLoggedIn')}
; @@ -163,16 +178,16 @@ export const User: FC = () => {
)} - {username !== "" && ( + {username && (
{usernameFieldLabel}
setUsername(e.target.value)} + onChange={e => setUsername({ ...username, value: e.target.value })} /> {
@@ -204,7 +219,45 @@ export const User: FC = () => {
)} -
+ {emails.length > 0 && ( +
+ {emailFieldLabel} +
+ {processUser.emails.map((email, _) => ( +
+ {email.value} +
+ ))} + {addingEmail ? ( +
+ setNewEmail(e.target.value)} + /> + + +
+ ) : ( + + )} +
+
+ )} + {/*
{processUser.emails.map((email, i) => (
{
))} -
+
*/}
{processUser.phoneNumbers.map((phone, i) => (
diff --git a/packages/shared-ui/src/styles/user.css b/packages/shared-ui/src/styles/user.css index 34c759baa..8d85b5330 100644 --- a/packages/shared-ui/src/styles/user.css +++ b/packages/shared-ui/src/styles/user.css @@ -118,6 +118,16 @@ font-family: var(--cb-font-family-primary); } +.cb-user-details-identifier-container { + display: flex; + flex-direction: row; + gap: 1rem; + border-radius: var(--cb-border-radius-sm); + border: var(--cb-border-color) 1px solid; + margin: 0.75rem 0; + padding: 0.6rem 0.5rem 0.6rem 1rem; +} + .cb-user-details-section-indentifier { width: 85%; align-self: self-start; From 901f6b73013595e5e034eebedd61034996af2717 Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Tue, 6 Aug 2024 14:41:39 +0200 Subject: [PATCH 09/55] progress --- .../react/src/components/ui/icons/AddIcon.tsx | 12 +- .../src/components/ui/icons/PendingIcon.tsx | 22 +++ .../src/components/ui/icons/PrimaryIcon.tsx | 22 +++ .../src/components/ui/icons/VerifiedIcon.tsx | 22 +++ .../src/contexts/CorbadoSessionContext.tsx | 15 +- .../src/contexts/CorbadoSessionProvider.tsx | 47 +++++- packages/react/src/screens/core/User.tsx | 138 +++++++++++------- packages/shared-ui/src/assets/pending.svg | 1 + packages/shared-ui/src/assets/primary.svg | 1 + packages/shared-ui/src/assets/verified.svg | 1 + packages/shared-ui/src/index.ts | 6 + packages/shared-ui/src/styles/user.css | 49 ++++++- packages/web-core/openapi/spec_v2.yaml | 27 ++++ packages/web-core/src/api/v2/api.ts | 137 +++++++++++++++++ .../web-core/src/services/SessionService.ts | 66 +++++++-- packages/web-js/src/core/Corbado.ts | 26 +++- 16 files changed, 519 insertions(+), 73 deletions(-) create mode 100644 packages/react/src/components/ui/icons/PendingIcon.tsx create mode 100644 packages/react/src/components/ui/icons/PrimaryIcon.tsx create mode 100644 packages/react/src/components/ui/icons/VerifiedIcon.tsx create mode 100644 packages/shared-ui/src/assets/pending.svg create mode 100644 packages/shared-ui/src/assets/primary.svg create mode 100644 packages/shared-ui/src/assets/verified.svg diff --git a/packages/react/src/components/ui/icons/AddIcon.tsx b/packages/react/src/components/ui/icons/AddIcon.tsx index a33e97ae3..d7a31808b 100644 --- a/packages/react/src/components/ui/icons/AddIcon.tsx +++ b/packages/react/src/components/ui/icons/AddIcon.tsx @@ -1,15 +1,19 @@ import addSrc from '@corbado/shared-ui/assets/add.svg'; import type { FC } from 'react'; -import { memo, useRef } from 'react'; +import { useRef } from 'react'; import React from 'react'; import { useIconWithTheme } from '../../../hooks/useIconWithTheme'; import type { IconProps } from './Icon'; import { Icon } from './Icon'; -export const AddIcon: FC = memo(props => { +export interface AddIconProps extends IconProps { + color?: 'primary' | 'secondary'; +} + +export const AddIcon: FC = ({ color, ...props }) => { const svgRef = useRef(null); - const { logoSVG } = useIconWithTheme(svgRef, addSrc, '--cb-button-text-primary-color'); + const { logoSVG } = useIconWithTheme(svgRef, addSrc, color === 'secondary' ? '--cb-text-primary-color' : '--cb-button-text-primary-color'); return ( = memo(props => { {...props} /> ); -}); +}; diff --git a/packages/react/src/components/ui/icons/PendingIcon.tsx b/packages/react/src/components/ui/icons/PendingIcon.tsx new file mode 100644 index 000000000..ceca4fc55 --- /dev/null +++ b/packages/react/src/components/ui/icons/PendingIcon.tsx @@ -0,0 +1,22 @@ +import pendingIconSrc from '@corbado/shared-ui/assets/pending.svg'; +import type { FC } from 'react'; +import { memo, useRef } from 'react'; +import React from 'react'; + +import { useIconWithTheme } from '../../../hooks/useIconWithTheme'; +import type { IconProps } from './Icon'; +import { Icon } from './Icon'; + +export const PendingIcon: FC = memo(props => { + const svgRef = useRef(null); + const { logoSVG } = useIconWithTheme(svgRef, pendingIconSrc, '--cb-passkey-list-badge-color'); + + return ( + + ); +}); diff --git a/packages/react/src/components/ui/icons/PrimaryIcon.tsx b/packages/react/src/components/ui/icons/PrimaryIcon.tsx new file mode 100644 index 000000000..80a4aaebd --- /dev/null +++ b/packages/react/src/components/ui/icons/PrimaryIcon.tsx @@ -0,0 +1,22 @@ +import primaryIconSrc from '@corbado/shared-ui/assets/primary.svg'; +import type { FC } from 'react'; +import { memo, useRef } from 'react'; +import React from 'react'; + +import { useIconWithTheme } from '../../../hooks/useIconWithTheme'; +import type { IconProps } from './Icon'; +import { Icon } from './Icon'; + +export const PrimaryIcon: FC = memo(props => { + const svgRef = useRef(null); + const { logoSVG } = useIconWithTheme(svgRef, primaryIconSrc, '--cb-passkey-list-badge-color'); + + return ( + + ); +}); diff --git a/packages/react/src/components/ui/icons/VerifiedIcon.tsx b/packages/react/src/components/ui/icons/VerifiedIcon.tsx new file mode 100644 index 000000000..33c3e2a48 --- /dev/null +++ b/packages/react/src/components/ui/icons/VerifiedIcon.tsx @@ -0,0 +1,22 @@ +import verifiedIconSrc from '@corbado/shared-ui/assets/verified.svg'; +import type { FC } from 'react'; +import { memo, useRef } from 'react'; +import React from 'react'; + +import { useIconWithTheme } from '../../../hooks/useIconWithTheme'; +import type { IconProps } from './Icon'; +import { Icon } from './Icon'; + +export const VerifiedIcon: FC = memo(props => { + const svgRef = useRef(null); + const { logoSVG } = useIconWithTheme(svgRef, verifiedIconSrc, '--cb-passkey-list-badge-color'); + + return ( + + ); +}); diff --git a/packages/react/src/contexts/CorbadoSessionContext.tsx b/packages/react/src/contexts/CorbadoSessionContext.tsx index 6891c894d..b9331096a 100644 --- a/packages/react/src/contexts/CorbadoSessionContext.tsx +++ b/packages/react/src/contexts/CorbadoSessionContext.tsx @@ -1,5 +1,6 @@ -import type { CorbadoUser, PassKeyList, SessionUser } from '@corbado/types'; +import type { CorbadoUser, LoginIdentifierType, PassKeyList, SessionUser } from '@corbado/types'; import type { CorbadoApp, CorbadoError, NonRecoverableError, PasskeyDeleteError } from '@corbado/web-core'; +import type { IdentifierListConfigRsp } from '@corbado/web-core/dist/api/v2'; import { createContext } from 'react'; import type { Result } from 'ts-results'; @@ -18,8 +19,14 @@ export interface CorbadoSessionContextProps { getPasskeys: (abortController?: AbortController) => Promise>; deletePasskey: (id: string) => Promise>; getFullUser: (abortController?: AbortController) => Promise>; + getIdentifierListConfig: (abortController?: AbortController) => Promise>; updateName: (fullName: string) => Promise>; updateUsername: (identifierID: string, username: string) => Promise>; + createIdentifier: (identifierType: LoginIdentifierType, value: string) => Promise>; + deleteIdentifier: (identifierID: string) => Promise>; + verifyIdentifierStart: (identifierID: string) => Promise>; + verifyIdentifierFinish: (identifierID: string, code: string) => Promise>; + deleteUser: () => Promise>; globalError: NonRecoverableError | undefined; } @@ -35,8 +42,14 @@ export const initialContext: CorbadoSessionContextProps = { getPasskeys: missingImplementation, deletePasskey: missingImplementation, getFullUser: missingImplementation, + getIdentifierListConfig: missingImplementation, updateName: missingImplementation, updateUsername: missingImplementation, + createIdentifier: missingImplementation, + deleteIdentifier: missingImplementation, + verifyIdentifierStart: missingImplementation, + verifyIdentifierFinish: missingImplementation, + deleteUser: missingImplementation, }; export const CorbadoSessionContext = createContext(initialContext); diff --git a/packages/react/src/contexts/CorbadoSessionProvider.tsx b/packages/react/src/contexts/CorbadoSessionProvider.tsx index 0c7d76a21..ccb0f54c8 100644 --- a/packages/react/src/contexts/CorbadoSessionProvider.tsx +++ b/packages/react/src/contexts/CorbadoSessionProvider.tsx @@ -1,4 +1,4 @@ -import type { CorbadoAppParams, SessionUser } from '@corbado/types'; +import type { CorbadoAppParams, LoginIdentifierType, SessionUser } from '@corbado/types'; import type { NonRecoverableError } from '@corbado/web-core'; import { CorbadoApp } from '@corbado/web-core'; import type { FC, PropsWithChildren } from 'react'; @@ -86,6 +86,13 @@ export const CorbadoSessionProvider: FC = ({ [corbadoApp], ); + const getIdentifierListConfig = useCallback( + (abortController?: AbortController) => { + return corbadoApp?.sessionService.getIdentifierListConfig(abortController ?? new AbortController()); + }, + [corbadoApp], + ); + const updateName = useCallback( (fullName: string) => { return corbadoApp.sessionService.updateName(fullName); @@ -100,6 +107,38 @@ export const CorbadoSessionProvider: FC = ({ [corbadoApp], ); + const createIdentifier = useCallback( + (identifierType: LoginIdentifierType, value: string) => { + return corbadoApp.sessionService.createIdentifier(identifierType, value); + }, + [corbadoApp], + ); + + const deleteIdentifier = useCallback( + (identifierID: string) => { + return corbadoApp.sessionService.deleteIdentifier(identifierID); + }, + [corbadoApp], + ); + + const verifyIdentifierStart = useCallback( + (identifierID: string) => { + return corbadoApp.sessionService.verifyIdentifierStart(identifierID); + }, + [corbadoApp], + ); + + const verifyIdentifierFinish = useCallback( + (identifierID: string, code: string) => { + return corbadoApp.sessionService.verifyIdentifierFinish(identifierID, code); + }, + [corbadoApp], + ); + + const deleteUser = useCallback(() => { + return corbadoApp.sessionService.deleteUser(); + }, [corbadoApp]); + return ( = ({ isAuthenticated, appendPasskey, getFullUser, + getIdentifierListConfig, updateName, updateUsername, + createIdentifier, + deleteIdentifier, + verifyIdentifierStart, + verifyIdentifierFinish, + deleteUser, getPasskeys, deletePasskey, logout, diff --git a/packages/react/src/screens/core/User.tsx b/packages/react/src/screens/core/User.tsx index 7d759b725..4b31fb276 100644 --- a/packages/react/src/screens/core/User.tsx +++ b/packages/react/src/screens/core/User.tsx @@ -1,14 +1,18 @@ +import { LoginIdentifierType } from '@corbado/shared-ui'; import { type CorbadoUser, type Identifier, type SocialAccount } from '@corbado/types'; +import { LoginIdentifierType1 } from '@corbado/web-core/dist/api/v2'; import type { FC } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, InputField, LoadingSpinner, PasskeyListErrorBoundary, PhoneInputField, Text } from '../../components'; +import { AddIcon } from '../../components/ui/icons/AddIcon'; import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; import { CopyIcon } from '../../components/ui/icons/CopyIcon'; +import { PendingIcon } from '../../components/ui/icons/PendingIcon'; +import { PrimaryIcon } from '../../components/ui/icons/PrimaryIcon'; +import { VerifiedIcon } from '../../components/ui/icons/VerifiedIcon'; import { useCorbado } from '../../hooks/useCorbado'; -import { LoginIdentifierType } from '@corbado/shared-ui'; -import { AddIcon } from '../../components/ui/icons/AddIcon'; interface ProcessedUser { name: string; @@ -19,12 +23,17 @@ interface ProcessedUser { } export const User: FC = () => { - const { corbadoApp, isAuthenticated, globalError, getFullUser, updateName, updateUsername } = useCorbado(); + const { corbadoApp, isAuthenticated, globalError, getFullUser, getIdentifierListConfig, updateName, updateUsername, createIdentifier } = useCorbado(); const { t } = useTranslation('translation', { keyPrefix: 'user' }); const [currentUser, setCurrentUser] = useState(); const [loading, setLoading] = useState(false); - const [name, setName] = useState(""); + const [fullNameRequired, setFullNameRequired] = useState(false); + const [usernameEnabled, setUsernameEnabled] = useState(false); + const [emailEnabled, setEmailEnabled] = useState(false); + // const [phoneEnabled, setPhoneEnabled] = useState(false); + + const [name, setName] = useState(); const [editingName, setEditingName] = useState(false); const [username, setUsername] = useState(); @@ -42,6 +51,7 @@ export const User: FC = () => { const abortController = new AbortController(); void getCurrentUser(abortController); + void getConfig(abortController); return () => { abortController.abort(); @@ -97,13 +107,44 @@ export const User: FC = () => { [corbadoApp], ); + const getConfig = useCallback( + async (abortController?: AbortController) => { + setLoading(true); + const result = await getIdentifierListConfig(abortController); + if (result.err && result.val.ignore) { + return; + } + + if (!result || result?.err) { + throw new Error(result?.val.name); + } + + setFullNameRequired(result.val.fullNameRequired); + for (const identifierConfig of result.val.identifiers) { + if (identifierConfig.type === LoginIdentifierType1.Custom) { + setUsernameEnabled(true); + } else if (identifierConfig.type === LoginIdentifierType1.Email) { + setEmailEnabled(true); + } else if (identifierConfig.type === LoginIdentifierType1.PhoneNumber) { + // setPhoneEnabled(true); + } + } + setLoading(false); + }, + [corbadoApp], + ); + const copyName = async () => { - await navigator.clipboard.writeText(name); + if (name) { + await navigator.clipboard.writeText(name); + } }; const changeName = async () => { // TODO: input checking? - await updateName(name); + if (name) { + await updateName(name); + } setEditingName(false); void getCurrentUser(); }; @@ -122,7 +163,10 @@ export const User: FC = () => { }; const addEmail = async () => { - return; + await createIdentifier(LoginIdentifierType.Email, newEmail); + setNewEmail(""); + setAddingEmail(false); + void getCurrentUser(); }; if (!isAuthenticated) { @@ -137,12 +181,13 @@ export const User: FC = () => {
{t('title')} - {name !== "" && ( + {fullNameRequired && (
{nameFieldLabel}
{
)} - {username && ( + {usernameEnabled && username && (
{usernameFieldLabel}
{
)} - {emails.length > 0 && ( + {emailEnabled && emails.length > 0 && (
{emailFieldLabel}
- {processUser.emails.map((email, _) => ( + {processUser.emails.map((email) => (
- {email.value} +
+ {email.value} +
+ {email.status === 'primary' ? ( +
+ + Primary +
+ ) : email.status === 'verified' ? ( +
+ + Verified +
+ ) : ( +
+ + Pending +
+ )} +
+
))} {addingEmail ? ( -
+
setNewEmail(e.target.value)} /> )}
)} - {/*
- {processUser.emails.map((email, i) => ( -
-
-
- -
-
- - {email.status === 'verified' ? verifiedText : unverifiedText} - -
-
-
- ))} -
*/}
{processUser.phoneNumbers.map((phone, i) => (
diff --git a/packages/shared-ui/src/assets/pending.svg b/packages/shared-ui/src/assets/pending.svg new file mode 100644 index 000000000..ca1aac7e5 --- /dev/null +++ b/packages/shared-ui/src/assets/pending.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/shared-ui/src/assets/primary.svg b/packages/shared-ui/src/assets/primary.svg new file mode 100644 index 000000000..d242e9bfe --- /dev/null +++ b/packages/shared-ui/src/assets/primary.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/shared-ui/src/assets/verified.svg b/packages/shared-ui/src/assets/verified.svg new file mode 100644 index 000000000..fc9a8160d --- /dev/null +++ b/packages/shared-ui/src/assets/verified.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/shared-ui/src/index.ts b/packages/shared-ui/src/index.ts index 84fb94f30..c8d242ca1 100644 --- a/packages/shared-ui/src/index.ts +++ b/packages/shared-ui/src/index.ts @@ -36,12 +36,15 @@ import passkeyDefaultIcon from './assets/passkey-default.svg'; import passkeyErrorIcon from './assets/passkey-error.svg'; import passkeyHybridIcon from './assets/passkey-hybrid.svg'; import passkeyHybridDarkIcon from './assets/passkey-hybrid-dark.svg'; +import pendingIcon from './assets/pending.svg'; import personIcon from './assets/person.svg'; import phoneIcon from './assets/phone.svg'; +import primaryIcon from './assets/primary.svg'; import rightIcon from './assets/right-arrow.svg'; import secureIcon from './assets/secure-icon.svg'; import shieldIcon from './assets/shield.svg'; import syncIcon from './assets/sync.svg'; +import verifiedIcon from './assets/verified.svg'; import visibilityIcon from './assets/visibility.svg'; import yahooIcon from './assets/yahoo.svg'; import i18nDe from './i18n/de.json'; @@ -100,4 +103,7 @@ export const assets = { lockIcon, copyIcon, changeIcon, + primaryIcon, + verifiedIcon, + pendingIcon, }; diff --git a/packages/shared-ui/src/styles/user.css b/packages/shared-ui/src/styles/user.css index 8d85b5330..1d32040b7 100644 --- a/packages/shared-ui/src/styles/user.css +++ b/packages/shared-ui/src/styles/user.css @@ -58,6 +58,11 @@ font-weight: bold; } +.cb-user-details-text { + font-size: calc(var(--cb-base-font-size) * 1.3); + font-family: var(--cb-primary-font); +} + .cb-user-details-body { width: 100%; max-width: 500px; @@ -87,7 +92,7 @@ background-color: white; cursor: pointer; box-sizing: border-box; - font-family: var(--cb-font-family-primary); + font-family: var(--cb-primary-font); } .cb-user-details-body-button-icon { @@ -104,7 +109,7 @@ border-radius: var(--cb-border-radius); cursor: pointer; box-sizing: border-box; - font-family: var(--cb-font-family-primary); + font-family: var(--cb-primary-font); } .cb-user-details-body-button-secondary { @@ -115,12 +120,10 @@ border-radius: var(--cb-border-radius); cursor: pointer; box-sizing: border-box; - font-family: var(--cb-font-family-primary); + font-family: var(--cb-primary-font); } .cb-user-details-identifier-container { - display: flex; - flex-direction: row; gap: 1rem; border-radius: var(--cb-border-radius-sm); border: var(--cb-border-color) 1px solid; @@ -128,6 +131,42 @@ padding: 0.6rem 0.5rem 0.6rem 1rem; } +.cb-user-details-header-badge-section { + margin: auto 0; + display: flex; + gap: 0.5rem; +} + +.cb-user-details-header-badge { + display: flex; + gap: 0.5rem; + flex-direction: row; + border-radius: var(--cb-border-radius-lg); + border: none; + color: var(--cb-passkey-list-badge-color); + background-color: var(--cb-passkey-list-badge-background-color); + padding: 0.35rem 0.7rem; + margin: auto; + align-items: center; + justify-content: center; + flex-grow: 1; +} + +.cb-user-details-header-badge-icon { + flex: 2; + height: 1rem; +} + +.cb-user-details-header-badge-text { + flex: 10; + font-weight: 500; + white-space: nowrap; + overflow: visible; + color: var(--cb-passkey-list-badge-color); + font-family: var(--cb-secondary-font); + font-size: calc(var(--cb-base-font-size) * 1.25); +} + .cb-user-details-section-indentifier { width: 85%; align-self: self-start; diff --git a/packages/web-core/openapi/spec_v2.yaml b/packages/web-core/openapi/spec_v2.yaml index 7f6d86c27..35f79dc15 100644 --- a/packages/web-core/openapi/spec_v2.yaml +++ b/packages/web-core/openapi/spec_v2.yaml @@ -43,6 +43,20 @@ paths: schema: $ref: '#/components/schemas/sessionConfigRsp' + /v2/identifier-list-config: + get: + description: Gets configs needed by the identifier-list component + operationId: GetIdentifierListConfig + tags: + - Configs + responses: + '200': + description: tbd + content: + application/json: + schema: + $ref: '#/components/schemas/identifierListConfigRsp' + /v2/me: get: description: Gets current user @@ -1124,6 +1138,19 @@ components: frontendApiUrl: type: string + identifierListConfigRsp: + type: object + required: + - fullNameRequired + - identifiers + properties: + fullNameRequired: + type: boolean + identifiers: + type: array + items: + $ref: 'common.yml#/components/schemas/loginIdentifierConfig' + mePasskeyRsp: type: object required: diff --git a/packages/web-core/src/api/v2/api.ts b/packages/web-core/src/api/v2/api.ts index e2827240c..5c0f16c9a 100644 --- a/packages/web-core/src/api/v2/api.ts +++ b/packages/web-core/src/api/v2/api.ts @@ -1124,6 +1124,25 @@ export interface Identifier { } +/** + * + * @export + * @interface IdentifierListConfigRsp + */ +export interface IdentifierListConfigRsp { + /** + * + * @type {boolean} + * @memberof IdentifierListConfigRsp + */ + 'fullNameRequired': boolean; + /** + * + * @type {Array} + * @memberof IdentifierListConfigRsp + */ + 'identifiers': Array; +} /** * * @export @@ -1245,6 +1264,46 @@ export interface LoginIdentifier { } +/** + * + * @export + * @interface LoginIdentifierConfig + */ +export interface LoginIdentifierConfig { + /** + * + * @type {LoginIdentifierType1} + * @memberof LoginIdentifierConfig + */ + 'type': LoginIdentifierType1; + /** + * + * @type {string} + * @memberof LoginIdentifierConfig + */ + 'enforceVerification': LoginIdentifierConfigEnforceVerificationEnum; + /** + * + * @type {boolean} + * @memberof LoginIdentifierConfig + */ + 'useAsLoginIdentifier': boolean; + /** + * + * @type {object} + * @memberof LoginIdentifierConfig + */ + 'metadata'?: object; +} + +export const LoginIdentifierConfigEnforceVerificationEnum = { + None: 'none', + Signup: 'signup', + AtFirstLogin: 'at_first_login' +} as const; + +export type LoginIdentifierConfigEnforceVerificationEnum = typeof LoginIdentifierConfigEnforceVerificationEnum[keyof typeof LoginIdentifierConfigEnforceVerificationEnum]; + /** * * @export @@ -1260,6 +1319,21 @@ export const LoginIdentifierType = { export type LoginIdentifierType = typeof LoginIdentifierType[keyof typeof LoginIdentifierType]; +/** + * Login Identifier type + * @export + * @enum {string} + */ + +export const LoginIdentifierType1 = { + Email: 'email', + PhoneNumber: 'phone_number', + Custom: 'custom' +} as const; + +export type LoginIdentifierType1 = typeof LoginIdentifierType1[keyof typeof LoginIdentifierType1]; + + /** * * @export @@ -3698,6 +3772,42 @@ export class AuthApi extends BaseAPI { */ export const ConfigsApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * Gets configs needed by the identifier-list component + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getIdentifierListConfig: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/v2/identifier-list-config`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + // authentication projectID required + await setApiKeyToObject(localVarHeaderParameter, "X-Corbado-ProjectID", configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * tbd * @param {*} [options] Override http request option. @@ -3744,6 +3854,15 @@ export const ConfigsApiAxiosParamCreator = function (configuration?: Configurati export const ConfigsApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = ConfigsApiAxiosParamCreator(configuration) return { + /** + * Gets configs needed by the identifier-list component + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getIdentifierListConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getIdentifierListConfig(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * tbd * @param {*} [options] Override http request option. @@ -3763,6 +3882,14 @@ export const ConfigsApiFp = function(configuration?: Configuration) { export const ConfigsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = ConfigsApiFp(configuration) return { + /** + * Gets configs needed by the identifier-list component + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getIdentifierListConfig(options?: any): AxiosPromise { + return localVarFp.getIdentifierListConfig(options).then((request) => request(axios, basePath)); + }, /** * tbd * @param {*} [options] Override http request option. @@ -3781,6 +3908,16 @@ export const ConfigsApiFactory = function (configuration?: Configuration, basePa * @extends {BaseAPI} */ export class ConfigsApi extends BaseAPI { + /** + * Gets configs needed by the identifier-list component + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ConfigsApi + */ + public getIdentifierListConfig(options?: AxiosRequestConfig) { + return ConfigsApiFp(this.configuration).getIdentifierListConfig(options).then((request) => request(this.axios, this.basePath)); + } + /** * tbd * @param {*} [options] Override http request option. diff --git a/packages/web-core/src/services/SessionService.ts b/packages/web-core/src/services/SessionService.ts index 4b7dffb06..bf0310a20 100644 --- a/packages/web-core/src/services/SessionService.ts +++ b/packages/web-core/src/services/SessionService.ts @@ -14,7 +14,7 @@ import { BehaviorSubject } from 'rxjs'; import { Err, Ok, Result } from 'ts-results'; import { Configuration } from '../api/v1'; -import type { SessionConfigRsp, ShortSessionCookieConfig } from '../api/v2'; +import type { IdentifierListConfigRsp, SessionConfigRsp, ShortSessionCookieConfig } from '../api/v2'; import { ConfigsApi, LoginIdentifierType, UsersApi } from '../api/v2'; import { ShortSession } from '../models/session'; import { @@ -45,6 +45,7 @@ const packageVersion = '0.0.0'; */ export class SessionService { #usersApi: UsersApi = new UsersApi(); + #configsApi: ConfigsApi; #webAuthnService: WebAuthnService; readonly #setShortSessionCookie: boolean; @@ -68,6 +69,13 @@ export class SessionService { this.#longSession = undefined; this.#setShortSessionCookie = setShortSessionCookie; this.#isPreviewMode = isPreviewMode; + + const config = new Configuration({ + apiKey: this.#projectId, + }); + + const axiosInstance = this.#createAxiosInstanceV2(); + this.#configsApi = new ConfigsApi(config, this.#getDefaultFrontendApiUrl(), axiosInstance); } /** @@ -157,6 +165,10 @@ export class SessionService { return this.wrapWithErr(async () => this.#usersApi.currentUserGet({ signal: abortController.signal })); } + public async getIdentifierListConfig(abortController: AbortController): Promise> { + return this.wrapWithErr(async () => this.#configsApi.getIdentifierListConfig({ signal: abortController.signal })); + } + async updateName(fullName: string): Promise> { return Result.wrapAsync(async () => { await this.#usersApi.currentUserUpdate({ fullName }); @@ -175,6 +187,49 @@ export class SessionService { }) } + async createIdentifier(identifierType: LoginIdentifierType, value: string): Promise> { + return Result.wrapAsync(async () => { + await this.#usersApi.currentUserIdentifierCreate({ identifierType, value }); + return void 0; + }); + } + + async deleteIdentifier(identifierID: string): Promise> { + return Result.wrapAsync(async () => { + await this.#usersApi.currentUserIdentifierDelete({ identifierID }); + return void 0; + }); + } + + async verifyIdentifierStart(identifierID: string): Promise> { + return Result.wrapAsync(async () => { + await this.#usersApi.currentUserIdentifierVerifyStart({ + identifierID, + clientInformation: { + bluetoothAvailable: (await WebAuthnService.canUseBluetooth()) ?? false, + canUsePasskeys: await WebAuthnService.doesBrowserSupportPasskeys(), + clientEnvHandle: WebAuthnService.getClientHandle() ?? undefined, + javaScriptHighEntropy: await WebAuthnService.getHighEntropyValues(), + }, + }); + return void 0; + }); + } + + async verifyIdentifierFinish(identifierID: string, code: string): Promise> { + return Result.wrapAsync(async () => { + await this.#usersApi.currentUserIdentifierVerifyFinish({ identifierID, code }); + return void 0; + }); + } + + async deleteUser(): Promise> { + return Result.wrapAsync(async () => { + await this.#usersApi.currentUserDelete({}); + return void 0; + }); + } + async appendPasskey(): Promise> { const canUsePasskeys = await WebAuthnService.doesBrowserSupportPasskeys(); @@ -546,15 +601,8 @@ export class SessionService { }; #loadSessionConfig = async (): Promise> => { - const config = new Configuration({ - apiKey: this.#projectId, - }); - - const axiosInstance = this.#createAxiosInstanceV2(); - const configsApi = new ConfigsApi(config, this.#getDefaultFrontendApiUrl(), axiosInstance); - return Result.wrapAsync(async () => { - const r = await configsApi.getSessionConfig(); + const r = await this.#configsApi.getSessionConfig(); return r.data; }); }; diff --git a/packages/web-js/src/core/Corbado.ts b/packages/web-js/src/core/Corbado.ts index 8c94877d7..8cbfbcae6 100644 --- a/packages/web-js/src/core/Corbado.ts +++ b/packages/web-js/src/core/Corbado.ts @@ -1,5 +1,5 @@ import { CorbadoAuth, Login, PasskeyList, SignUp, User } from '@corbado/react'; -import type { CorbadoAuthConfig, CorbadoLoginConfig, CorbadoSignUpConfig } from '@corbado/types'; +import type { CorbadoAuthConfig, CorbadoLoginConfig, CorbadoSignUpConfig, LoginIdentifierType } from '@corbado/types'; import type { FC } from 'react'; import type { Root } from 'react-dom/client'; @@ -106,6 +106,10 @@ export class Corbado { return this.#getCorbadoAppState().corbadoApp.sessionService.getFullUser(abortController ?? new AbortController()); } + getIdentifierListConfig(abortController?: AbortController) { + return this.#getCorbadoAppState().corbadoApp.sessionService.getIdentifierListConfig(abortController ?? new AbortController()); + } + updateName(fullName: string) { return this.#getCorbadoAppState().corbadoApp.sessionService.updateName(fullName); } @@ -114,6 +118,26 @@ export class Corbado { return this.#getCorbadoAppState().corbadoApp.sessionService.updateUsername(identifierID, username); } + createIdentifier(identifierType: LoginIdentifierType, value: string) { + return this.#getCorbadoAppState().corbadoApp.sessionService.createIdentifier(identifierType, value); + } + + deleteIdentifier(identifierID: string) { + return this.#getCorbadoAppState().corbadoApp.sessionService.deleteIdentifier(identifierID); + } + + verifyIdentifierStart(identifierID: string) { + return this.#getCorbadoAppState().corbadoApp.sessionService.verifyIdentifierStart(identifierID); + } + + verifyIdentifierFinish(identifierID: string, code: string) { + return this.#getCorbadoAppState().corbadoApp.sessionService.verifyIdentifierFinish(identifierID, code); + } + + deleteUser() { + return this.#getCorbadoAppState().corbadoApp.sessionService.deleteUser(); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any #mountComponent = >(element: HTMLElement, Component: FC, componentOptions: T) => { if (!this.#corbadoAppState) { From d5e8fcfaff3288e8f7277e24c7abf1a8477ee20f Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Tue, 6 Aug 2024 16:04:16 +0200 Subject: [PATCH 10/55] identifier list config needs to be debugged --- .../src/contexts/CorbadoSessionContext.tsx | 5 +-- packages/react/src/screens/core/User.tsx | 9 ++-- .../shared-ui/src/flowHandler/constants.ts | 6 +++ packages/types/src/session.ts | 43 +++++++++++++++++++ .../web-core/src/services/SessionService.ts | 6 +-- 5 files changed, 58 insertions(+), 11 deletions(-) diff --git a/packages/react/src/contexts/CorbadoSessionContext.tsx b/packages/react/src/contexts/CorbadoSessionContext.tsx index b9331096a..25e3e144b 100644 --- a/packages/react/src/contexts/CorbadoSessionContext.tsx +++ b/packages/react/src/contexts/CorbadoSessionContext.tsx @@ -1,6 +1,5 @@ -import type { CorbadoUser, LoginIdentifierType, PassKeyList, SessionUser } from '@corbado/types'; +import type { CorbadoUser, IdentifierListConfig, LoginIdentifierType, PassKeyList, SessionUser } from '@corbado/types'; import type { CorbadoApp, CorbadoError, NonRecoverableError, PasskeyDeleteError } from '@corbado/web-core'; -import type { IdentifierListConfigRsp } from '@corbado/web-core/dist/api/v2'; import { createContext } from 'react'; import type { Result } from 'ts-results'; @@ -19,7 +18,7 @@ export interface CorbadoSessionContextProps { getPasskeys: (abortController?: AbortController) => Promise>; deletePasskey: (id: string) => Promise>; getFullUser: (abortController?: AbortController) => Promise>; - getIdentifierListConfig: (abortController?: AbortController) => Promise>; + getIdentifierListConfig: (abortController?: AbortController) => Promise>; updateName: (fullName: string) => Promise>; updateUsername: (identifierID: string, username: string) => Promise>; createIdentifier: (identifierType: LoginIdentifierType, value: string) => Promise>; diff --git a/packages/react/src/screens/core/User.tsx b/packages/react/src/screens/core/User.tsx index 4b31fb276..d8ef0d71b 100644 --- a/packages/react/src/screens/core/User.tsx +++ b/packages/react/src/screens/core/User.tsx @@ -1,6 +1,5 @@ -import { LoginIdentifierType } from '@corbado/shared-ui'; +import { LoginIdentifierConfigType, LoginIdentifierType } from '@corbado/shared-ui'; import { type CorbadoUser, type Identifier, type SocialAccount } from '@corbado/types'; -import { LoginIdentifierType1 } from '@corbado/web-core/dist/api/v2'; import type { FC } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -121,11 +120,11 @@ export const User: FC = () => { setFullNameRequired(result.val.fullNameRequired); for (const identifierConfig of result.val.identifiers) { - if (identifierConfig.type === LoginIdentifierType1.Custom) { + if (identifierConfig.type === LoginIdentifierConfigType.Username) { setUsernameEnabled(true); - } else if (identifierConfig.type === LoginIdentifierType1.Email) { + } else if (identifierConfig.type === LoginIdentifierConfigType.Email) { setEmailEnabled(true); - } else if (identifierConfig.type === LoginIdentifierType1.PhoneNumber) { + } else if (identifierConfig.type === LoginIdentifierConfigType.Phone) { // setPhoneEnabled(true); } } diff --git a/packages/shared-ui/src/flowHandler/constants.ts b/packages/shared-ui/src/flowHandler/constants.ts index 0d4fa1295..e972a72d3 100644 --- a/packages/shared-ui/src/flowHandler/constants.ts +++ b/packages/shared-ui/src/flowHandler/constants.ts @@ -25,6 +25,12 @@ export const createLoginIdentifierType = (v: ApiLoginIdentifierType): LoginIdent } }; +export enum LoginIdentifierConfigType { + Email = 'email', + Phone = 'phone_number', + Username = 'custom', +} + // Enum representing the names of different sign up flows export enum SignUpFlowNames { PasskeySignupWithFallback = 'PasskeySignupWithFallback', diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 505cc6a33..9b86d9c9c 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -88,3 +88,46 @@ export const LoginIdentifierType = { * @typedef {string} LoginIdentifierType */ export type LoginIdentifierType = (typeof LoginIdentifierType)[keyof typeof LoginIdentifierType]; + +/** + * Object for LoginIdentifierConfigType. + * @typedef {Object} LoginIdentifierConfigType + * @property {string} Email - Represents an email identifier. + * @property {string} Phone - Represents a phone identifier. + * @property {string} Username - Represents a username identifier. + */ +export const LoginIdentifierConfigType = { + Email: 'email', + Phone: 'phone_number', + Username: 'custom', +} as const; + +/** + * Type for LoginIdentifierType. + * @typedef {string} LoginIdentifierType + */ +export type LoginIdentifierConfigType = (typeof LoginIdentifierConfigType)[keyof typeof LoginIdentifierConfigType]; + +/** + * Interface for IdentifierConfig. + * @interface + * @property {LoginIdentifierConfigType} type - The type of the identifier. + * @property {string} enforceVerification - Indicates verification policy. + * @property {boolean} useAsLoginIdentifier - Indicates used for login. + */ +export interface IdentifierConfig { + type: LoginIdentifierConfigType; + enforceVerification: string; + useAsLoginIdentifier: boolean; +} + +/** + * Interface for IdentifierConfig. + * @interface + * @property {boolean} fullNameRequired - Indicates if full name is required. + * @property {Array} identifiers - Config for each identifier type. + */ +export interface IdentifierListConfig { + fullNameRequired: boolean; + identifiers: Array; +} diff --git a/packages/web-core/src/services/SessionService.ts b/packages/web-core/src/services/SessionService.ts index bf0310a20..635781346 100644 --- a/packages/web-core/src/services/SessionService.ts +++ b/packages/web-core/src/services/SessionService.ts @@ -1,5 +1,5 @@ /// <- add this line -import type { CorbadoUser, PassKeyList, SessionUser } from '@corbado/types'; +import type { CorbadoUser, IdentifierListConfig, PassKeyList, SessionUser } from '@corbado/types'; import type { AxiosHeaders, AxiosInstance, @@ -14,7 +14,7 @@ import { BehaviorSubject } from 'rxjs'; import { Err, Ok, Result } from 'ts-results'; import { Configuration } from '../api/v1'; -import type { IdentifierListConfigRsp, SessionConfigRsp, ShortSessionCookieConfig } from '../api/v2'; +import type { SessionConfigRsp, ShortSessionCookieConfig } from '../api/v2'; import { ConfigsApi, LoginIdentifierType, UsersApi } from '../api/v2'; import { ShortSession } from '../models/session'; import { @@ -165,7 +165,7 @@ export class SessionService { return this.wrapWithErr(async () => this.#usersApi.currentUserGet({ signal: abortController.signal })); } - public async getIdentifierListConfig(abortController: AbortController): Promise> { + public async getIdentifierListConfig(abortController: AbortController): Promise> { return this.wrapWithErr(async () => this.#configsApi.getIdentifierListConfig({ signal: abortController.signal })); } From 9a40d61f2977973171b62b23440583f015e12e54 Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Mon, 12 Aug 2024 13:16:20 +0200 Subject: [PATCH 11/55] incorporate identifier-list-config --- packages/react/src/screens/core/User.tsx | 216 ++++++++++++++++------- 1 file changed, 150 insertions(+), 66 deletions(-) diff --git a/packages/react/src/screens/core/User.tsx b/packages/react/src/screens/core/User.tsx index d8ef0d71b..c5177ebdd 100644 --- a/packages/react/src/screens/core/User.tsx +++ b/packages/react/src/screens/core/User.tsx @@ -22,7 +22,7 @@ interface ProcessedUser { } export const User: FC = () => { - const { corbadoApp, isAuthenticated, globalError, getFullUser, getIdentifierListConfig, updateName, updateUsername, createIdentifier } = useCorbado(); + const { corbadoApp, isAuthenticated, globalError, getFullUser, getIdentifierListConfig, updateName, updateUsername, createIdentifier, deleteIdentifier } = useCorbado(); const { t } = useTranslation('translation', { keyPrefix: 'user' }); const [currentUser, setCurrentUser] = useState(); const [loading, setLoading] = useState(false); @@ -36,6 +36,7 @@ export const User: FC = () => { const [editingName, setEditingName] = useState(false); const [username, setUsername] = useState(); + const [addingUsername, setAddingUsername] = useState(false); const [editingUsername, setEditingUsername] = useState(false); const [emails, setEmails] = useState([]); @@ -152,6 +153,14 @@ export const User: FC = () => { await navigator.clipboard.writeText(username?.value || ""); }; + const addUsername = async () => { + if (username) { + await createIdentifier(LoginIdentifierType.Username, username?.value || ""); + } + setAddingUsername(false); + void getCurrentUser(); + }; + const changeUsername = async () => { // TODO: input checking? if (username) { @@ -168,6 +177,15 @@ export const User: FC = () => { void getCurrentUser(); }; + const removeEmail = async (email: Identifier) => { + await deleteIdentifier(email.id); + void getCurrentUser(); + }; + + const startEmailVerification = (email: Identifier) => { + return email; + }; + if (!isAuthenticated) { return
{t('warning_notLoggedIn')}
; } @@ -184,91 +202,147 @@ export const User: FC = () => {
{nameFieldLabel}
-
- setName(e.target.value)} - /> - void copyName()} - /> -
- {editingName ? ( -
- - -
- ) : ( + {!processUser.name && !editingName ? ( + ) : ( +
+
+ setName(e.target.value)} + /> + void copyName()} + /> +
+ {editingName ? ( +
+ + +
+ ) : ( + + )} +
)}
)} - {usernameEnabled && username && ( + {usernameEnabled && (
{usernameFieldLabel}
-
- setUsername({ ...username, value: e.target.value })} - /> - void copyUsername()} - /> -
- {editingUsername ? ( + {!processUser.username ? (
- - + {addingUsername ? ( +
+
+ setUsername({ id: "", type: "username", status: "verified", value: e.target.value })} + /> + void copyUsername()} + /> +
+ + +
+ ) : ( + + )}
) : ( - +
+ {username && ( +
+
+ setUsername({ ...username, value: e.target.value })} + /> + void copyUsername()} + /> +
+ {editingUsername ? ( +
+ + +
+ ) : ( + + )} +
+ )} +
)}
)} - {emailEnabled && emails.length > 0 && ( + {emailEnabled && (
{emailFieldLabel}
- {processUser.emails.map((email) => ( + {emails.map((email) => (
{email.value} @@ -290,6 +364,16 @@ export const User: FC = () => {
)}
+ +
))} @@ -318,7 +402,7 @@ export const User: FC = () => { className='cb-user-details-body-button' onClick={() => setAddingEmail(true)}> - Add Another + Add Email )}
From bec747d84c9aa7610785e092763ccb94fc43e1a3 Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Mon, 12 Aug 2024 17:38:51 +0200 Subject: [PATCH 12/55] error handling --- packages/react/src/screens/core/User.tsx | 207 +++++++++++++++++------ 1 file changed, 152 insertions(+), 55 deletions(-) diff --git a/packages/react/src/screens/core/User.tsx b/packages/react/src/screens/core/User.tsx index c5177ebdd..7325690aa 100644 --- a/packages/react/src/screens/core/User.tsx +++ b/packages/react/src/screens/core/User.tsx @@ -22,8 +22,8 @@ interface ProcessedUser { } export const User: FC = () => { - const { corbadoApp, isAuthenticated, globalError, getFullUser, getIdentifierListConfig, updateName, updateUsername, createIdentifier, deleteIdentifier } = useCorbado(); - const { t } = useTranslation('translation', { keyPrefix: 'user' }); + const { corbadoApp, isAuthenticated, globalError, getFullUser, getIdentifierListConfig, updateName, updateUsername, createIdentifier, deleteIdentifier, verifyIdentifierStart, verifyIdentifierFinish } = useCorbado(); + const { t } = useTranslation('translation'); const [currentUser, setCurrentUser] = useState(); const [loading, setLoading] = useState(false); @@ -40,6 +40,8 @@ export const User: FC = () => { const [editingUsername, setEditingUsername] = useState(false); const [emails, setEmails] = useState([]); + const [verifyingEmails, setVerifyingEmails] = useState([]); + const [challengeCodes, setChallengeCodes] = useState([]); const [addingEmail, setAddingEmail] = useState(false); const [newEmail, setNewEmail] = useState(""); @@ -58,13 +60,13 @@ export const User: FC = () => { }; }, [isAuthenticated]); - const nameFieldLabel = useMemo(() => t('name'), [t]); - const usernameFieldLabel = useMemo(() => t('username'), [t]); - const emailFieldLabel = useMemo(() => t('email'), [t]); - const phoneFieldLabel = useMemo(() => t('phone'), [t]); - const socialFieldLabel = useMemo(() => t('social'), [t]); - const verifiedText = useMemo(() => t('verified'), [t]); - const unverifiedText = useMemo(() => t('unverified'), [t]); + const nameFieldLabel = useMemo(() => t('user.name'), [t]); + const usernameFieldLabel = useMemo(() => t('user.username'), [t]); + const emailFieldLabel = useMemo(() => t('user.email'), [t]); + const phoneFieldLabel = useMemo(() => t('user.phone'), [t]); + const socialFieldLabel = useMemo(() => t('user.social'), [t]); + const verifiedText = useMemo(() => t('user.verified'), [t]); + const unverifiedText = useMemo(() => t('user.unverified'), [t]); const processUser = useMemo((): ProcessedUser => { if (!currentUser) { return { @@ -101,7 +103,10 @@ export const User: FC = () => { setName(result.val.fullName || ""); const usernameIdentifier = result.val.identifiers.find(identifier => identifier.type == LoginIdentifierType.Username); setUsername(usernameIdentifier); - setEmails(result.val.identifiers.filter(identifier => identifier.type == LoginIdentifierType.Email)); + const emails = result.val.identifiers.filter(identifier => identifier.type == LoginIdentifierType.Email); + setEmails(emails); + setVerifyingEmails(emails.map(() => false)); + setChallengeCodes(emails.map(() => "")); setLoading(false); }, [corbadoApp], @@ -141,9 +146,15 @@ export const User: FC = () => { }; const changeName = async () => { - // TODO: input checking? - if (name) { - await updateName(name); + if (!name) { + console.error("name is empty"); + return; + } + const res = await updateName(name); + if (res.err) { + // no possible error code + console.error(res.val.message); + return; } setEditingName(false); void getCurrentUser(); @@ -154,40 +165,104 @@ export const User: FC = () => { }; const addUsername = async () => { - if (username) { - await createIdentifier(LoginIdentifierType.Username, username?.value || ""); + if (!username || !username.value) { + console.error("username is empty"); + return; + } + const res = await createIdentifier(LoginIdentifierType.Username, username?.value || ""); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible code: unsupported_identifier_type (but the current UI flow should prevent this, because unsupported types are not shown) + console.error(t(`errors.${code}`)); + } + return; } setAddingUsername(false); void getCurrentUser(); }; const changeUsername = async () => { - // TODO: input checking? - if (username) { - await updateUsername(username.id, username.value); + if (!username || !username.value) { + console.error("username is empty"); + return; + } + const res = await updateUsername(username.id, username.value); + if (res.err) { + // no possible error code + console.error(res.val.message); + return; } setEditingUsername(false); void getCurrentUser(); }; const addEmail = async () => { - await createIdentifier(LoginIdentifierType.Email, newEmail); + if (!newEmail) { + console.error("email is empty"); + return; + } + const res = await createIdentifier(LoginIdentifierType.Email, newEmail); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible code: unsupported_identifier_type (but the current UI flow should prevent this, because unsupported types are not shown) + console.error(t(`errors.${code}`)); + } + return; + } setNewEmail(""); setAddingEmail(false); void getCurrentUser(); }; - const removeEmail = async (email: Identifier) => { - await deleteIdentifier(email.id); + const removeEmail = async (index: number) => { + const res = await deleteIdentifier(emails[index].id); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible codes: no_remaining_identifier, no_remaining_verified_identifier + console.error(t(`errors.${code}`)); + } + return; + } void getCurrentUser(); }; - const startEmailVerification = (email: Identifier) => { - return email; + const startEmailVerification = async (index: number) => { + const res = await verifyIdentifierStart(emails[index].id); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible code: wait_before_retry + console.error(t(`errors.${code}`)); + } + return; + } + setVerifyingEmails(verifyingEmails.map((v, i) => (i === index) ? true : v)); + }; + + const finishEmailVerification = async (index: number) => { + const res = await verifyIdentifierFinish(emails[index].id, challengeCodes[index]); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible code: invalid_challenge_solution_{verification_type} + console.error(t(`errors.${code}`)); + } + } else { + void getCurrentUser(); + } + }; + + const getErrorCode = (message: string) => { + const regex = /\(([^)]+)\)/; + const matches = regex.exec(message); + return matches ? matches[1] : undefined; }; if (!isAuthenticated) { - return
{t('warning_notLoggedIn')}
; + return
{t('user.warning_notLoggedIn')}
; } if (loading) { @@ -197,7 +272,7 @@ export const User: FC = () => { return (
- {t('title')} + {t('user.title')} {fullNameRequired && (
{nameFieldLabel} @@ -342,39 +417,61 @@ export const User: FC = () => {
{emailFieldLabel}
- {emails.map((email) => ( + {emails.map((email, index) => (
-
- {email.value} -
- {email.status === 'primary' ? ( -
- - Primary -
- ) : email.status === 'verified' ? ( -
- - Verified -
- ) : ( -
- - Pending -
+ {verifyingEmails[index] ? ( +
+ Enter OTP code for: {email.value} + setChallengeCodes(challengeCodes.map((c, i) => i === index ? e.target.value : c))} + /> + + +
+ ) : ( +
+ {email.value} +
+ {email.status === 'primary' ? ( +
+ + Primary +
+ ) : email.status === 'verified' ? ( +
+ + Verified +
+ ) : ( +
+ + Pending +
+ )} +
+ {email.status === 'pending' && ( + )} +
- - -
+ )}
))} {addingEmail ? ( From 7be1634cda72902708988ad445659a4b25332ff5 Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Mon, 12 Aug 2024 17:58:02 +0200 Subject: [PATCH 13/55] phone --- packages/react/src/screens/core/User.tsx | 210 ++++++++++++++++++----- 1 file changed, 167 insertions(+), 43 deletions(-) diff --git a/packages/react/src/screens/core/User.tsx b/packages/react/src/screens/core/User.tsx index 7325690aa..216fbee84 100644 --- a/packages/react/src/screens/core/User.tsx +++ b/packages/react/src/screens/core/User.tsx @@ -4,7 +4,7 @@ import type { FC } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, InputField, LoadingSpinner, PasskeyListErrorBoundary, PhoneInputField, Text } from '../../components'; +import { Button, InputField, LoadingSpinner, PasskeyListErrorBoundary, Text } from '../../components'; import { AddIcon } from '../../components/ui/icons/AddIcon'; import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; import { CopyIcon } from '../../components/ui/icons/CopyIcon'; @@ -30,7 +30,7 @@ export const User: FC = () => { const [fullNameRequired, setFullNameRequired] = useState(false); const [usernameEnabled, setUsernameEnabled] = useState(false); const [emailEnabled, setEmailEnabled] = useState(false); - // const [phoneEnabled, setPhoneEnabled] = useState(false); + const [phoneEnabled, setPhoneEnabled] = useState(false); const [name, setName] = useState(); const [editingName, setEditingName] = useState(false); @@ -41,10 +41,15 @@ export const User: FC = () => { const [emails, setEmails] = useState([]); const [verifyingEmails, setVerifyingEmails] = useState([]); - const [challengeCodes, setChallengeCodes] = useState([]); + const [emailChallengeCodes, setEmailChallengeCodes] = useState([]); const [addingEmail, setAddingEmail] = useState(false); const [newEmail, setNewEmail] = useState(""); + const [phones, setPhones] = useState([]); + const [verifyingPhones, setVerifyingPhones] = useState([]); + const [phoneChallengeCodes, setPhoneChallengeCodes] = useState([]); + const [addingPhone, setAddingPhone] = useState(false); + const [newPhone, setNewPhone] = useState(""); useEffect(() => { if (!isAuthenticated) { @@ -65,8 +70,6 @@ export const User: FC = () => { const emailFieldLabel = useMemo(() => t('user.email'), [t]); const phoneFieldLabel = useMemo(() => t('user.phone'), [t]); const socialFieldLabel = useMemo(() => t('user.social'), [t]); - const verifiedText = useMemo(() => t('user.verified'), [t]); - const unverifiedText = useMemo(() => t('user.unverified'), [t]); const processUser = useMemo((): ProcessedUser => { if (!currentUser) { return { @@ -106,7 +109,11 @@ export const User: FC = () => { const emails = result.val.identifiers.filter(identifier => identifier.type == LoginIdentifierType.Email); setEmails(emails); setVerifyingEmails(emails.map(() => false)); - setChallengeCodes(emails.map(() => "")); + setEmailChallengeCodes(emails.map(() => "")); + const phones = result.val.identifiers.filter(identifier => identifier.type == LoginIdentifierType.Phone); + setPhones(phones); + setVerifyingPhones(phones.map(() => false)); + setPhoneChallengeCodes(phones.map(() => "")); setLoading(false); }, [corbadoApp], @@ -131,7 +138,7 @@ export const User: FC = () => { } else if (identifierConfig.type === LoginIdentifierConfigType.Email) { setEmailEnabled(true); } else if (identifierConfig.type === LoginIdentifierConfigType.Phone) { - // setPhoneEnabled(true); + setPhoneEnabled(true); } } setLoading(false); @@ -243,16 +250,74 @@ export const User: FC = () => { }; const finishEmailVerification = async (index: number) => { - const res = await verifyIdentifierFinish(emails[index].id, challengeCodes[index]); + const res = await verifyIdentifierFinish(emails[index].id, emailChallengeCodes[index]); if (res.err) { const code = getErrorCode(res.val.message); if (code) { - // possible code: invalid_challenge_solution_{verification_type} + // possible code: invalid_challenge_solution_email-otp console.error(t(`errors.${code}`)); } - } else { - void getCurrentUser(); + return; + } + void getCurrentUser(); + }; + + const addPhone = async () => { + if (!newPhone) { + console.error("phone is empty"); + return; + } + const res = await createIdentifier(LoginIdentifierType.Phone, newPhone); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible code: unsupported_identifier_type (but the current UI flow should prevent this, because unsupported types are not shown) + console.error(t(`errors.${code}`)); + } + return; + } + setNewPhone(""); + setAddingPhone(false); + void getCurrentUser(); + }; + + const removePhone = async (index: number) => { + const res = await deleteIdentifier(phones[index].id); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible codes: no_remaining_identifier, no_remaining_verified_identifier + console.error(t(`errors.${code}`)); + } + return; } + void getCurrentUser(); + }; + + const startPhoneVerification = async (index: number) => { + const res = await verifyIdentifierStart(phones[index].id); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible code: wait_before_retry + console.error(t(`errors.${code}`)); + } + return; + } + setVerifyingPhones(verifyingPhones.map((v, i) => (i === index) ? true : v)); + }; + + const finishPhoneVerification = async (index: number) => { + const res = await verifyIdentifierFinish(phones[index].id, phoneChallengeCodes[index]); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible code: invalid_challenge_solution_phone-otp + console.error(t(`errors.${code}`)); + } + return; + } + void getCurrentUser(); }; const getErrorCode = (message: string) => { @@ -424,7 +489,7 @@ export const User: FC = () => { Enter OTP code for: {email.value} setChallengeCodes(challengeCodes.map((c, i) => i === index ? e.target.value : c))} + onChange={e => setEmailChallengeCodes(emailChallengeCodes.map((c, i) => i === index ? e.target.value : c))} />
)} -
- {processUser.phoneNumbers.map((phone, i) => ( -
-
-
- + {phoneEnabled && ( +
+ {phoneFieldLabel} +
+ {phones.map((phone, index) => ( +
+ {verifyingPhones[index] ? ( +
+ Enter OTP code for: {phone.value} + setPhoneChallengeCodes(phoneChallengeCodes.map((c, i) => i === index ? e.target.value : c))} + /> + + +
+ ) : ( +
+ {phone.value} +
+ {phone.status === 'primary' ? ( +
+ + Primary +
+ ) : phone.status === 'verified' ? ( +
+ + Verified +
+ ) : ( +
+ + Pending +
+ )} +
+ {phone.status === 'pending' && ( + + )} + +
+ )}
-
- - {phone.status === 'verified' ? verifiedText : unverifiedText} - + ))} + {addingPhone ? ( +
+ setNewPhone(e.target.value)} + /> + +
-
+ ) : ( + + )}
- ))} -
+
+ )} +
{processUser.socialAccounts.map((social, i) => (
From 778114336249450780bbdef3499a658662dd214d Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Tue, 13 Aug 2024 11:05:41 +0200 Subject: [PATCH 14/55] prettier --- .../react/src/components/ui/icons/AddIcon.tsx | 6 +- packages/react/src/screens/core/User.tsx | 219 ++++++++++++------ .../web-core/src/services/SessionService.ts | 6 +- packages/web-js/src/core/Corbado.ts | 4 +- 4 files changed, 156 insertions(+), 79 deletions(-) diff --git a/packages/react/src/components/ui/icons/AddIcon.tsx b/packages/react/src/components/ui/icons/AddIcon.tsx index d7a31808b..caab1773f 100644 --- a/packages/react/src/components/ui/icons/AddIcon.tsx +++ b/packages/react/src/components/ui/icons/AddIcon.tsx @@ -13,7 +13,11 @@ export interface AddIconProps extends IconProps { export const AddIcon: FC = ({ color, ...props }) => { const svgRef = useRef(null); - const { logoSVG } = useIconWithTheme(svgRef, addSrc, color === 'secondary' ? '--cb-text-primary-color' : '--cb-button-text-primary-color'); + const { logoSVG } = useIconWithTheme( + svgRef, + addSrc, + color === 'secondary' ? '--cb-text-primary-color' : '--cb-button-text-primary-color', + ); return ( { - const { corbadoApp, isAuthenticated, globalError, getFullUser, getIdentifierListConfig, updateName, updateUsername, createIdentifier, deleteIdentifier, verifyIdentifierStart, verifyIdentifierFinish } = useCorbado(); + const { + corbadoApp, + isAuthenticated, + globalError, + getFullUser, + getIdentifierListConfig, + updateName, + updateUsername, + createIdentifier, + deleteIdentifier, + verifyIdentifierStart, + verifyIdentifierFinish, + } = useCorbado(); const { t } = useTranslation('translation'); const [currentUser, setCurrentUser] = useState(); const [loading, setLoading] = useState(false); @@ -43,13 +55,13 @@ export const User: FC = () => { const [verifyingEmails, setVerifyingEmails] = useState([]); const [emailChallengeCodes, setEmailChallengeCodes] = useState([]); const [addingEmail, setAddingEmail] = useState(false); - const [newEmail, setNewEmail] = useState(""); + const [newEmail, setNewEmail] = useState(''); const [phones, setPhones] = useState([]); const [verifyingPhones, setVerifyingPhones] = useState([]); const [phoneChallengeCodes, setPhoneChallengeCodes] = useState([]); const [addingPhone, setAddingPhone] = useState(false); - const [newPhone, setNewPhone] = useState(""); + const [newPhone, setNewPhone] = useState(''); useEffect(() => { if (!isAuthenticated) { @@ -103,17 +115,19 @@ export const User: FC = () => { } setCurrentUser(result.val); - setName(result.val.fullName || ""); - const usernameIdentifier = result.val.identifiers.find(identifier => identifier.type == LoginIdentifierType.Username); + setName(result.val.fullName || ''); + const usernameIdentifier = result.val.identifiers.find( + identifier => identifier.type == LoginIdentifierType.Username, + ); setUsername(usernameIdentifier); const emails = result.val.identifiers.filter(identifier => identifier.type == LoginIdentifierType.Email); setEmails(emails); setVerifyingEmails(emails.map(() => false)); - setEmailChallengeCodes(emails.map(() => "")); + setEmailChallengeCodes(emails.map(() => '')); const phones = result.val.identifiers.filter(identifier => identifier.type == LoginIdentifierType.Phone); setPhones(phones); setVerifyingPhones(phones.map(() => false)); - setPhoneChallengeCodes(phones.map(() => "")); + setPhoneChallengeCodes(phones.map(() => '')); setLoading(false); }, [corbadoApp], @@ -154,7 +168,7 @@ export const User: FC = () => { const changeName = async () => { if (!name) { - console.error("name is empty"); + console.error('name is empty'); return; } const res = await updateName(name); @@ -168,15 +182,15 @@ export const User: FC = () => { }; const copyUsername = async () => { - await navigator.clipboard.writeText(username?.value || ""); + await navigator.clipboard.writeText(username?.value || ''); }; const addUsername = async () => { if (!username || !username.value) { - console.error("username is empty"); + console.error('username is empty'); return; } - const res = await createIdentifier(LoginIdentifierType.Username, username?.value || ""); + const res = await createIdentifier(LoginIdentifierType.Username, username?.value || ''); if (res.err) { const code = getErrorCode(res.val.message); if (code) { @@ -191,7 +205,7 @@ export const User: FC = () => { const changeUsername = async () => { if (!username || !username.value) { - console.error("username is empty"); + console.error('username is empty'); return; } const res = await updateUsername(username.id, username.value); @@ -206,7 +220,7 @@ export const User: FC = () => { const addEmail = async () => { if (!newEmail) { - console.error("email is empty"); + console.error('email is empty'); return; } const res = await createIdentifier(LoginIdentifierType.Email, newEmail); @@ -218,7 +232,7 @@ export const User: FC = () => { } return; } - setNewEmail(""); + setNewEmail(''); setAddingEmail(false); void getCurrentUser(); }; @@ -246,7 +260,7 @@ export const User: FC = () => { } return; } - setVerifyingEmails(verifyingEmails.map((v, i) => (i === index) ? true : v)); + setVerifyingEmails(verifyingEmails.map((v, i) => (i === index ? true : v))); }; const finishEmailVerification = async (index: number) => { @@ -264,7 +278,7 @@ export const User: FC = () => { const addPhone = async () => { if (!newPhone) { - console.error("phone is empty"); + console.error('phone is empty'); return; } const res = await createIdentifier(LoginIdentifierType.Phone, newPhone); @@ -276,7 +290,7 @@ export const User: FC = () => { } return; } - setNewPhone(""); + setNewPhone(''); setAddingPhone(false); void getCurrentUser(); }; @@ -304,7 +318,7 @@ export const User: FC = () => { } return; } - setVerifyingPhones(verifyingPhones.map((v, i) => (i === index) ? true : v)); + setVerifyingPhones(verifyingPhones.map((v, i) => (i === index ? true : v))); }; const finishPhoneVerification = async (index: number) => { @@ -344,9 +358,13 @@ export const User: FC = () => {
{!processUser.name && !editingName ? ( ) : ( @@ -368,20 +386,26 @@ export const User: FC = () => { {editingName ? (
) : ( @@ -404,7 +428,9 @@ export const User: FC = () => { className='cb-user-details-text' // key={`user-entry-${processUser.username}`} value={username?.value} - onChange={e => setUsername({ id: "", type: "username", status: "verified", value: e.target.value })} + onChange={e => + setUsername({ id: '', type: 'username', status: 'verified', value: e.target.value }) + } /> { />
) : ( )} @@ -453,20 +488,26 @@ export const User: FC = () => { {editingUsername ? (
) : ( @@ -489,16 +530,20 @@ export const User: FC = () => { Enter OTP code for: {email.value} setEmailChallengeCodes(emailChallengeCodes.map((c, i) => i === index ? e.target.value : c))} + onChange={e => + setEmailChallengeCodes(emailChallengeCodes.map((c, i) => (i === index ? e.target.value : c))) + } />
@@ -525,14 +570,16 @@ export const User: FC = () => {
{email.status === 'pending' && ( )}
@@ -543,27 +590,35 @@ export const User: FC = () => {
setNewEmail(e.target.value)} />
) : ( )} @@ -581,16 +636,20 @@ export const User: FC = () => { Enter OTP code for: {phone.value} setPhoneChallengeCodes(phoneChallengeCodes.map((c, i) => i === index ? e.target.value : c))} + onChange={e => + setPhoneChallengeCodes(phoneChallengeCodes.map((c, i) => (i === index ? e.target.value : c))) + } />
@@ -617,14 +676,16 @@ export const User: FC = () => {
{phone.status === 'pending' && ( )}
@@ -635,27 +696,35 @@ export const User: FC = () => {
setNewPhone(e.target.value)} />
) : ( )} diff --git a/packages/web-core/src/services/SessionService.ts b/packages/web-core/src/services/SessionService.ts index 635781346..a322f8dcb 100644 --- a/packages/web-core/src/services/SessionService.ts +++ b/packages/web-core/src/services/SessionService.ts @@ -165,7 +165,9 @@ export class SessionService { return this.wrapWithErr(async () => this.#usersApi.currentUserGet({ signal: abortController.signal })); } - public async getIdentifierListConfig(abortController: AbortController): Promise> { + public async getIdentifierListConfig( + abortController: AbortController, + ): Promise> { return this.wrapWithErr(async () => this.#configsApi.getIdentifierListConfig({ signal: abortController.signal })); } @@ -184,7 +186,7 @@ export class SessionService { value: username, }); return void 0; - }) + }); } async createIdentifier(identifierType: LoginIdentifierType, value: string): Promise> { diff --git a/packages/web-js/src/core/Corbado.ts b/packages/web-js/src/core/Corbado.ts index 8cbfbcae6..b7ada23f2 100644 --- a/packages/web-js/src/core/Corbado.ts +++ b/packages/web-js/src/core/Corbado.ts @@ -107,7 +107,9 @@ export class Corbado { } getIdentifierListConfig(abortController?: AbortController) { - return this.#getCorbadoAppState().corbadoApp.sessionService.getIdentifierListConfig(abortController ?? new AbortController()); + return this.#getCorbadoAppState().corbadoApp.sessionService.getIdentifierListConfig( + abortController ?? new AbortController(), + ); } updateName(fullName: string) { From 77971b3dda92cfade83b0ab2157b730d151592b0 Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Tue, 13 Aug 2024 13:13:51 +0200 Subject: [PATCH 15/55] add translations, add delete-user card --- packages/react/src/screens/core/User.tsx | 126 ++++++++++++++++------- packages/shared-ui/src/i18n/en.json | 17 ++- packages/shared-ui/src/styles/user.css | 4 - 3 files changed, 101 insertions(+), 46 deletions(-) diff --git a/packages/react/src/screens/core/User.tsx b/packages/react/src/screens/core/User.tsx index d7a893704..7fc9812c6 100644 --- a/packages/react/src/screens/core/User.tsx +++ b/packages/react/src/screens/core/User.tsx @@ -34,6 +34,7 @@ export const User: FC = () => { deleteIdentifier, verifyIdentifierStart, verifyIdentifierFinish, + deleteUser, } = useCorbado(); const { t } = useTranslation('translation'); const [currentUser, setCurrentUser] = useState(); @@ -77,11 +78,32 @@ export const User: FC = () => { }; }, [isAuthenticated]); - const nameFieldLabel = useMemo(() => t('user.name'), [t]); - const usernameFieldLabel = useMemo(() => t('user.username'), [t]); - const emailFieldLabel = useMemo(() => t('user.email'), [t]); - const phoneFieldLabel = useMemo(() => t('user.phone'), [t]); - const socialFieldLabel = useMemo(() => t('user.social'), [t]); + const title = useMemo(() => t('user.title'), [t]); + + const headerName = useMemo(() => t('user.name'), [t]); + const headerUsername = useMemo(() => t('user.username'), [t]); + const headerEmail = useMemo(() => t('user.email'), [t]); + const headerPhone = useMemo(() => t('user.phone'), [t]); + // const headerSocial = useMemo(() => t('user.social'), [t]); + const headerDelete = useMemo(() => t('user.delete_account'), [t]); + + const textDelete = useMemo(() => t('user.delete_account_text'), [t]); + + const badgePrimary = useMemo(() => t('user.primary'), [t]); + const badgeVerified = useMemo(() => t('user.verified'), [t]); + const badgePending = useMemo(() => t('user.pending'), [t]); + + const buttonVerify = useMemo(() => t('user.verify'), [t]); + const buttonRemove = useMemo(() => t('user.remove'), [t]); + const buttonDelete = useMemo(() => t('user.delete'), [t]); + const buttonSave = useMemo(() => t('user.save'), [t]); + const buttonCancel = useMemo(() => t('user.cancel'), [t]); + const buttonChange = useMemo(() => t('user.change'), [t]); + const buttonAddName = useMemo(() => t('user.add_name'), [t]); + const buttonAddUsername = useMemo(() => t('user.add_username'), [t]); + const buttonAddEmail = useMemo(() => t('user.add_email'), [t]); + const buttonAddPhone = useMemo(() => t('user.add_phone'), [t]); + const processUser = useMemo((): ProcessedUser => { if (!currentUser) { return { @@ -334,6 +356,16 @@ export const User: FC = () => { void getCurrentUser(); }; + const deleteAccount = async () => { + const res = await deleteUser(); + if (res.err) { + // no possible error code + console.error(res.val.message); + return; + } + // TODO: go back to login page? + }; + const getErrorCode = (message: string) => { const regex = /\(([^)]+)\)/; const matches = regex.exec(message); @@ -351,10 +383,10 @@ export const User: FC = () => { return (
- {t('user.title')} + {title} {fullNameRequired && (
- {nameFieldLabel} + {headerName}
{!processUser.name && !editingName ? ( ) : (
@@ -389,7 +421,7 @@ export const User: FC = () => { className='cb-user-details-body-button-primary' onClick={() => void changeName()} > - Save + {buttonSave}
) : ( @@ -407,7 +439,7 @@ export const User: FC = () => { onClick={() => setEditingName(true)} > - Change + {buttonChange} )}
@@ -417,7 +449,7 @@ export const User: FC = () => { )} {usernameEnabled && (
- {usernameFieldLabel} + {headerUsername}
{!processUser.username ? (
@@ -442,7 +474,7 @@ export const User: FC = () => { className='cb-user-details-body-button-primary' onClick={() => void addUsername()} > - Save + {buttonSave}
) : ( @@ -463,7 +495,7 @@ export const User: FC = () => { color='secondary' className='cb-user-details-body-button-icon' /> - Add Username + {buttonAddUsername} )}
@@ -491,7 +523,7 @@ export const User: FC = () => { className='cb-user-details-body-button-primary' onClick={() => void changeUsername()} > - Save + {buttonSave}
) : ( @@ -509,7 +541,7 @@ export const User: FC = () => { onClick={() => setEditingUsername(true)} > - Change + {buttonChange} )}
@@ -521,7 +553,7 @@ export const User: FC = () => { )} {emailEnabled && (
- {emailFieldLabel} + {headerEmail}
{emails.map((email, index) => (
@@ -544,7 +576,7 @@ export const User: FC = () => { className='cb-user-details-body-button-primary' onClick={() => setVerifyingEmails(verifyingEmails.map((v, i) => (i === index ? false : v)))} > - Cancel + {buttonCancel}
) : ( @@ -554,17 +586,17 @@ export const User: FC = () => { {email.status === 'primary' ? (
- Primary + {badgePrimary}
) : email.status === 'verified' ? (
- Verified + {badgeVerified}
) : (
- Pending + {badgePending}
)}
@@ -573,14 +605,14 @@ export const User: FC = () => { className='cb-user-details-body-button-primary' onClick={() => void startEmailVerification(index)} > - Verify + {buttonVerify} )}
)} @@ -599,7 +631,7 @@ export const User: FC = () => { className='cb-user-details-body-button-primary' onClick={() => void addEmail()} > - Save + {buttonSave}
) : ( @@ -619,7 +651,7 @@ export const User: FC = () => { color='secondary' className='cb-user-details-body-button-icon' /> - Add Email + {buttonAddEmail} )}
@@ -627,7 +659,7 @@ export const User: FC = () => { )} {phoneEnabled && (
- {phoneFieldLabel} + {headerPhone}
{phones.map((phone, index) => (
@@ -650,7 +682,7 @@ export const User: FC = () => { className='cb-user-details-body-button-primary' onClick={() => setVerifyingPhones(verifyingPhones.map((v, i) => (i === index ? false : v)))} > - Cancel + {buttonCancel}
) : ( @@ -660,17 +692,17 @@ export const User: FC = () => { {phone.status === 'primary' ? (
- Primary + {badgePrimary}
) : phone.status === 'verified' ? (
- Verified + {badgeVerified}
) : (
- Pending + {badgePending}
)}
@@ -679,14 +711,14 @@ export const User: FC = () => { className='cb-user-details-body-button-primary' onClick={() => void startPhoneVerification(index)} > - Verify + {buttonVerify} )}
)} @@ -705,7 +737,7 @@ export const User: FC = () => { className='cb-user-details-body-button-primary' onClick={() => void addPhone()} > - Save + {buttonSave}
) : ( @@ -725,14 +757,14 @@ export const User: FC = () => { color='secondary' className='cb-user-details-body-button-icon' /> - Add Phone + {buttonAddPhone} )}
)} -
+ {/*
{processUser.socialAccounts.map((social, i) => (
{ @@ -761,6 +793,20 @@ export const User: FC = () => {
))} +
*/} + +
+ {headerDelete} +
+
+ {textDelete} +
+
+
diff --git a/packages/shared-ui/src/i18n/en.json b/packages/shared-ui/src/i18n/en.json index b017b405a..37d9c234b 100644 --- a/packages/shared-ui/src/i18n/en.json +++ b/packages/shared-ui/src/i18n/en.json @@ -414,8 +414,21 @@ "email": "Email address", "phone": "Phone number", "social": "Social accounts", - "verified": "verified", - "unverified": "pending", + "delete_account": "Delete account", + "delete_account_text": "This will permanently remove your personal account and all of its content.", + "primary": "Primary", + "verified": "Verified", + "pending": "Pending", + "verify": "Verify", + "remove": "Remove", + "save": "Save", + "cancel": "Cancel", + "change": "Change", + "add_name": "Add name", + "add_username": "Add username", + "add_email": "Add email", + "add_phone": "Add phone", + "delete": "Delete", "warning_notLoggedIn": "Please log in to see your current user information.", "providers": { "google": "Google", diff --git a/packages/shared-ui/src/styles/user.css b/packages/shared-ui/src/styles/user.css index 1d32040b7..43df472e2 100644 --- a/packages/shared-ui/src/styles/user.css +++ b/packages/shared-ui/src/styles/user.css @@ -39,10 +39,6 @@ overflow: hidden; } -.cb-user-details-card:hover { - border: var(--cb-passkey-list-border-hover-color) 1px solid; -} - .cb-user-details-header { margin: auto 0; margin-bottom: 0.5rem; From da33a84fe4acd187cbb8033ee6e2d6433e13f120 Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Tue, 13 Aug 2024 13:41:59 +0200 Subject: [PATCH 16/55] prettier --- packages/react/src/screens/core/User.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/react/src/screens/core/User.tsx b/packages/react/src/screens/core/User.tsx index 7fc9812c6..df90392e3 100644 --- a/packages/react/src/screens/core/User.tsx +++ b/packages/react/src/screens/core/User.tsx @@ -92,7 +92,7 @@ export const User: FC = () => { const badgePrimary = useMemo(() => t('user.primary'), [t]); const badgeVerified = useMemo(() => t('user.verified'), [t]); const badgePending = useMemo(() => t('user.pending'), [t]); - + const buttonVerify = useMemo(() => t('user.verify'), [t]); const buttonRemove = useMemo(() => t('user.remove'), [t]); const buttonDelete = useMemo(() => t('user.delete'), [t]); @@ -797,14 +797,15 @@ export const User: FC = () => {
{headerDelete} -
-
+
+
{textDelete} -
-
+
+
From 9ddd6ed569739a4877039bb85aa4b8433f0b742d Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Tue, 13 Aug 2024 15:02:52 +0200 Subject: [PATCH 17/55] change component name to userdetails --- packages/react/src/index.ts | 4 +- .../core/{User.tsx => UserDetails.tsx} | 48 ++++++++++--------- packages/shared-ui/src/i18n/en.json | 2 +- packages/shared-ui/src/styles/index.css | 2 +- .../src/styles/{user.css => user-details.css} | 0 packages/web-js/src/core/Corbado.ts | 4 +- .../react/src/components/AuthDetails.tsx | 4 +- 7 files changed, 33 insertions(+), 31 deletions(-) rename packages/react/src/screens/core/{User.tsx => UserDetails.tsx} (95%) rename packages/shared-ui/src/styles/{user.css => user-details.css} (100%) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index a5d656d68..00bc648c2 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -7,14 +7,14 @@ import CorbadoAuth from './screens/core/CorbadoAuth'; import Login from './screens/core/Login'; import PasskeyList from './screens/core/PasskeyList'; import SignUp from './screens/core/SignUp'; -import { User } from './screens/core/User'; +import UserDetails from './screens/core/UserDetails'; export { CorbadoProvider, useCorbado, CorbadoAuth, + UserDetails, PasskeyList, - User, SignUp, Login, CorbadoThemes, diff --git a/packages/react/src/screens/core/User.tsx b/packages/react/src/screens/core/UserDetails.tsx similarity index 95% rename from packages/react/src/screens/core/User.tsx rename to packages/react/src/screens/core/UserDetails.tsx index df90392e3..302b4999d 100644 --- a/packages/react/src/screens/core/User.tsx +++ b/packages/react/src/screens/core/UserDetails.tsx @@ -21,7 +21,7 @@ interface ProcessedUser { socialAccounts: SocialAccount[]; } -export const User: FC = () => { +export const UserDetails: FC = () => { const { corbadoApp, isAuthenticated, @@ -78,31 +78,31 @@ export const User: FC = () => { }; }, [isAuthenticated]); - const title = useMemo(() => t('user.title'), [t]); + const title = useMemo(() => t('user-details.title'), [t]); - const headerName = useMemo(() => t('user.name'), [t]); - const headerUsername = useMemo(() => t('user.username'), [t]); - const headerEmail = useMemo(() => t('user.email'), [t]); - const headerPhone = useMemo(() => t('user.phone'), [t]); - // const headerSocial = useMemo(() => t('user.social'), [t]); - const headerDelete = useMemo(() => t('user.delete_account'), [t]); + const headerName = useMemo(() => t('user-details.name'), [t]); + const headerUsername = useMemo(() => t('user-details.username'), [t]); + const headerEmail = useMemo(() => t('user-details.email'), [t]); + const headerPhone = useMemo(() => t('user-details.phone'), [t]); + // const headerSocial = useMemo(() => t('user-details.social'), [t]); + const headerDelete = useMemo(() => t('user-details.delete_account'), [t]); - const textDelete = useMemo(() => t('user.delete_account_text'), [t]); + const textDelete = useMemo(() => t('user-details.delete_account_text'), [t]); - const badgePrimary = useMemo(() => t('user.primary'), [t]); - const badgeVerified = useMemo(() => t('user.verified'), [t]); - const badgePending = useMemo(() => t('user.pending'), [t]); + const badgePrimary = useMemo(() => t('user-details.primary'), [t]); + const badgeVerified = useMemo(() => t('user-details.verified'), [t]); + const badgePending = useMemo(() => t('user-details.pending'), [t]); - const buttonVerify = useMemo(() => t('user.verify'), [t]); - const buttonRemove = useMemo(() => t('user.remove'), [t]); - const buttonDelete = useMemo(() => t('user.delete'), [t]); - const buttonSave = useMemo(() => t('user.save'), [t]); - const buttonCancel = useMemo(() => t('user.cancel'), [t]); - const buttonChange = useMemo(() => t('user.change'), [t]); - const buttonAddName = useMemo(() => t('user.add_name'), [t]); - const buttonAddUsername = useMemo(() => t('user.add_username'), [t]); - const buttonAddEmail = useMemo(() => t('user.add_email'), [t]); - const buttonAddPhone = useMemo(() => t('user.add_phone'), [t]); + const buttonVerify = useMemo(() => t('user-details.verify'), [t]); + const buttonRemove = useMemo(() => t('user-details.remove'), [t]); + const buttonDelete = useMemo(() => t('user-details.delete'), [t]); + const buttonSave = useMemo(() => t('user-details.save'), [t]); + const buttonCancel = useMemo(() => t('user-details.cancel'), [t]); + const buttonChange = useMemo(() => t('user-details.change'), [t]); + const buttonAddName = useMemo(() => t('user-details.add_name'), [t]); + const buttonAddUsername = useMemo(() => t('user-details.add_username'), [t]); + const buttonAddEmail = useMemo(() => t('user-details.add_email'), [t]); + const buttonAddPhone = useMemo(() => t('user-details.add_phone'), [t]); const processUser = useMemo((): ProcessedUser => { if (!currentUser) { @@ -373,7 +373,7 @@ export const User: FC = () => { }; if (!isAuthenticated) { - return
{t('user.warning_notLoggedIn')}
; + return
{t('user-details.warning_notLoggedIn')}
; } if (loading) { @@ -813,3 +813,5 @@ export const User: FC = () => { ); }; + +export default UserDetails; diff --git a/packages/shared-ui/src/i18n/en.json b/packages/shared-ui/src/i18n/en.json index 37d9c234b..d16b8aec4 100644 --- a/packages/shared-ui/src/i18n/en.json +++ b/packages/shared-ui/src/i18n/en.json @@ -407,7 +407,7 @@ "button_confirm": "Ok" } }, - "user": { + "user-details": { "title": "User Details", "name": "Name", "username": "Username", diff --git a/packages/shared-ui/src/styles/index.css b/packages/shared-ui/src/styles/index.css index 52b0fe25b..21b30b680 100644 --- a/packages/shared-ui/src/styles/index.css +++ b/packages/shared-ui/src/styles/index.css @@ -7,7 +7,7 @@ @import './common.css'; @import './buttons.css'; @import './inputs.css'; -@import './user.css'; +@import './user-details.css'; @import './corbado-auth.css'; @import './passkey-list.css'; @import './themes/emerald-funk.css'; diff --git a/packages/shared-ui/src/styles/user.css b/packages/shared-ui/src/styles/user-details.css similarity index 100% rename from packages/shared-ui/src/styles/user.css rename to packages/shared-ui/src/styles/user-details.css diff --git a/packages/web-js/src/core/Corbado.ts b/packages/web-js/src/core/Corbado.ts index b7ada23f2..ef3c9c53b 100644 --- a/packages/web-js/src/core/Corbado.ts +++ b/packages/web-js/src/core/Corbado.ts @@ -1,4 +1,4 @@ -import { CorbadoAuth, Login, PasskeyList, SignUp, User } from '@corbado/react'; +import { CorbadoAuth, Login, PasskeyList, SignUp, UserDetails } from '@corbado/react'; import type { CorbadoAuthConfig, CorbadoLoginConfig, CorbadoSignUpConfig, LoginIdentifierType } from '@corbado/types'; import type { FC } from 'react'; import type { Root } from 'react-dom/client'; @@ -75,7 +75,7 @@ export class Corbado { } mountUserUI(element: HTMLElement) { - this.#mountComponent(element, User, {}); + this.#mountComponent(element, UserDetails, {}); } unmountUserUI(element: HTMLElement) { diff --git a/playground/react/src/components/AuthDetails.tsx b/playground/react/src/components/AuthDetails.tsx index d3df43875..2bb374255 100644 --- a/playground/react/src/components/AuthDetails.tsx +++ b/playground/react/src/components/AuthDetails.tsx @@ -1,4 +1,4 @@ -import { PasskeyList, useCorbado, User } from '@corbado/react'; +import { UserDetails, PasskeyList, useCorbado } from '@corbado/react'; import { useNavigate, useParams } from 'react-router-dom'; export const AuthDetails = () => { @@ -9,7 +9,7 @@ export const AuthDetails = () => { return (
- + - ) : ( -
-
- setName(e.target.value)} - /> - void copyName()} - /> -
- {editingName ? ( -
- - -
- ) : ( - - )} -
- )} -
-
- )} + {usernameEnabled && (
{headerUsername} diff --git a/packages/react/src/screens/user-details-blocks/NameEdit.tsx b/packages/react/src/screens/user-details-blocks/NameEdit.tsx new file mode 100644 index 000000000..83cf7aca9 --- /dev/null +++ b/packages/react/src/screens/user-details-blocks/NameEdit.tsx @@ -0,0 +1,7 @@ +import React, { FC } from 'react'; + +const NameEdit: FC = () => { + return
NameEdit
; +}; + +export default NameEdit; From 26a8259ac33285359f4a26b75ba590186c0d914c Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Fri, 16 Aug 2024 10:26:24 +0200 Subject: [PATCH 19/55] refactors corbado user details logic into a context --- .../contexts/CorbadoUserDetailsContext.tsx | 54 +++++++ .../contexts/CorbadoUserDetailsProvider.tsx | 135 ++++++++++++++++++ .../react/src/hooks/useCorbadoUserDetails.ts | 13 ++ .../screens/user-details-blocks/NameEdit.tsx | 109 +++++++++++++- 4 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 packages/react/src/contexts/CorbadoUserDetailsContext.tsx create mode 100644 packages/react/src/contexts/CorbadoUserDetailsProvider.tsx create mode 100644 packages/react/src/hooks/useCorbadoUserDetails.ts diff --git a/packages/react/src/contexts/CorbadoUserDetailsContext.tsx b/packages/react/src/contexts/CorbadoUserDetailsContext.tsx new file mode 100644 index 000000000..15075b357 --- /dev/null +++ b/packages/react/src/contexts/CorbadoUserDetailsContext.tsx @@ -0,0 +1,54 @@ +import { createContext } from 'react'; +import { Identifier, SocialAccount } from '@corbado/types'; + +const missingImplementation = (): never => { + throw new Error('Please make sure that your components are wrapped inside '); +}; + +interface ProcessedUser { + name: string; + username: string; + emails: Identifier[]; + phoneNumbers: Identifier[]; + socialAccounts: SocialAccount[]; +} + +export interface CorbadoUserDetailsContextProps { + loading: boolean; + processUser: ProcessedUser | undefined; + name: string | undefined; + setName: (name: string) => void; + username: Identifier | undefined; + setUsername: (username: Identifier | undefined) => void; + emails: Identifier[] | undefined; + setEmails: (email: Identifier[] | undefined) => void; + phones: Identifier[] | undefined; + userNameEnabled: boolean; + emailEnabled: boolean; + phoneEnabled: boolean; + fullNameRequired: boolean; + setPhones: (phone: Identifier[] | undefined) => void; + getCurrentUser: (abortController?: AbortController) => Promise; + getConfig: (abortController?: AbortController) => Promise; +} + +export const initialContext: CorbadoUserDetailsContextProps = { + loading: false, + processUser: undefined, + name: undefined, + setName: missingImplementation, + username: undefined, + setUsername: missingImplementation, + emails: undefined, + setEmails: missingImplementation, + phones: undefined, + userNameEnabled: false, + emailEnabled: false, + phoneEnabled: false, + fullNameRequired: false, + setPhones: missingImplementation, + getCurrentUser: missingImplementation, + getConfig: missingImplementation, +}; + +export const CorbadoUserDetailsContext = createContext(initialContext); diff --git a/packages/react/src/contexts/CorbadoUserDetailsProvider.tsx b/packages/react/src/contexts/CorbadoUserDetailsProvider.tsx new file mode 100644 index 000000000..c7dc745cc --- /dev/null +++ b/packages/react/src/contexts/CorbadoUserDetailsProvider.tsx @@ -0,0 +1,135 @@ +import type { FC, PropsWithChildren } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { CorbadoUserDetailsContext } from './CorbadoUserDetailsContext'; +import { CorbadoUser, Identifier, LoginIdentifierConfigType, LoginIdentifierType } from '@corbado/types'; +import { useCorbado } from '../hooks/useCorbado'; + +export const CorbadoUserDetailsProvider: FC = ({ children }) => { + const { corbadoApp, isAuthenticated, getFullUser, getIdentifierListConfig } = useCorbado(); + + const [loading, setLoading] = useState(false); + const [currentUser, setCurrentUser] = useState(); + const [name, setName] = useState(); + const [username, setUsername] = useState(); + const [emails, setEmails] = useState(); + const [phones, setPhones] = useState(); + + const [usernameEnabled, setUsernameEnabled] = useState(false); + const [emailEnabled, setEmailEnabled] = useState(false); + const [phoneEnabled, setPhoneEnabled] = useState(false); + + const [fullNameRequired, setFullNameRequired] = useState(false); + + useEffect(() => { + if (!isAuthenticated) { + return; + } + + const abortController = new AbortController(); + void getCurrentUser(abortController); + void getConfig(abortController); + + return () => { + abortController.abort(); + }; + }, [isAuthenticated]); + + const processUser = useMemo(() => { + if (!currentUser) { + return { + name: '', + username: '', + emails: [], + phoneNumbers: [], + socialAccounts: [], + }; + } + + return { + name: currentUser.fullName, + username: currentUser.identifiers.find(id => id.type === 'username')?.value || '', + emails: currentUser.identifiers.filter(id => id.type === 'email'), + phoneNumbers: currentUser.identifiers.filter(id => id.type === 'phone'), + socialAccounts: currentUser.socialAccounts, + }; + }, [currentUser]); + + const getCurrentUser = useCallback( + async (abortController?: AbortController) => { + setLoading(true); + const result = await getFullUser(abortController); + if (result.err && result.val.ignore) { + return; + } + + if (!result || result?.err) { + throw new Error(result?.val.name); + } + + setCurrentUser(result.val); + setName(result.val.fullName || ''); + const usernameIdentifier = result.val.identifiers.find( + identifier => identifier.type == LoginIdentifierType.Username, + ); + setUsername(usernameIdentifier); + const emails = result.val.identifiers.filter(identifier => identifier.type == LoginIdentifierType.Email); + setEmails(emails); + const phones = result.val.identifiers.filter(identifier => identifier.type == LoginIdentifierType.Phone); + setPhones(phones); + setLoading(false); + }, + [corbadoApp], + ); + + const getConfig = useCallback( + async (abortController?: AbortController) => { + setLoading(true); + const result = await getIdentifierListConfig(abortController); + if (result.err && result.val.ignore) { + return; + } + + if (!result || result?.err) { + throw new Error(result?.val.name); + } + + setFullNameRequired(result.val.fullNameRequired); + for (const identifierConfig of result.val.identifiers) { + if (identifierConfig.type === LoginIdentifierConfigType.Username) { + setUsernameEnabled(true); + } else if (identifierConfig.type === LoginIdentifierConfigType.Email) { + setEmailEnabled(true); + } else if (identifierConfig.type === LoginIdentifierConfigType.Phone) { + setPhoneEnabled(true); + } + } + setLoading(false); + }, + [corbadoApp], + ); + + return ( + + {children} + + ); +}; diff --git a/packages/react/src/hooks/useCorbadoUserDetails.ts b/packages/react/src/hooks/useCorbadoUserDetails.ts new file mode 100644 index 000000000..76913b6ac --- /dev/null +++ b/packages/react/src/hooks/useCorbadoUserDetails.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react'; + +import { CorbadoUserDetailsContext, CorbadoUserDetailsContextProps } from '../contexts/CorbadoUserDetailsContext'; + +export const useCorbadoUserDetails = (): CorbadoUserDetailsContextProps => { + const corbadoUserDetails = useContext(CorbadoUserDetailsContext); + + if (!corbadoUserDetails) { + throw new Error('Please make sure that your components are wrapped inside '); + } + + return corbadoUserDetails; +}; diff --git a/packages/react/src/screens/user-details-blocks/NameEdit.tsx b/packages/react/src/screens/user-details-blocks/NameEdit.tsx index 83cf7aca9..3ff016e5c 100644 --- a/packages/react/src/screens/user-details-blocks/NameEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/NameEdit.tsx @@ -1,7 +1,112 @@ -import React, { FC } from 'react'; +import React, { FC, useMemo, useState } from 'react'; +import { Button, InputField, Text } from '../../components'; +import { AddIcon } from '../../components/ui/icons/AddIcon'; +import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; +import { CopyIcon } from '../../components/ui/icons/CopyIcon'; +import { useTranslation } from 'react-i18next'; +import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; +import { useCorbado } from '../..'; const NameEdit: FC = () => { - return
NameEdit
; + const { updateName } = useCorbado(); + const { name, getCurrentUser, processUser, setName, fullNameRequired } = useCorbadoUserDetails(); + const { t } = useTranslation('translation'); + + const [editingName, setEditingName] = useState(false); + + const headerName = useMemo(() => t('user-details.name'), [t]); + const buttonAddName = useMemo(() => t('user-details.add_name'), [t]); + + const buttonSave = useMemo(() => t('user-details.save'), [t]); + const buttonCancel = useMemo(() => t('user-details.cancel'), [t]); + const buttonChange = useMemo(() => t('user-details.change'), [t]); + + const copyName = async () => { + if (name) { + await navigator.clipboard.writeText(name); + } + }; + + const changeName = async () => { + if (!name) { + console.error('name is empty'); + return; + } + const res = await updateName(name); + if (res.err) { + // no possible error code + console.error(res.val.message); + return; + } + setEditingName(false); + void getCurrentUser(); + }; + + if (!processUser || !fullNameRequired) return; + + return ( +
+ {headerName} +
+ {!processUser.name && !editingName ? ( + + ) : ( +
+
+ setName(e.target.value)} + /> + void copyName()} + /> +
+ {editingName ? ( +
+ + +
+ ) : ( + + )} +
+ )} +
+
+ ); }; export default NameEdit; From 9031db51d01a8d4fe2b25d184fb41eda999490dc Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Fri, 16 Aug 2024 11:22:12 +0200 Subject: [PATCH 20/55] refactors UserDetails screen --- .../contexts/CorbadoUserDetailsContext.tsx | 4 +- .../contexts/CorbadoUserDetailsProvider.tsx | 8 +- .../react/src/screens/core/UserDetails.tsx | 723 +----------------- .../user-details-blocks/EmailsEdit.tsx | 202 +++++ .../user-details-blocks/PhonesEdit.tsx | 202 +++++ .../user-details-blocks/UserDelete.tsx | 41 + .../user-details-blocks/UsernameEdit.tsx | 168 ++++ packages/react/src/util.ts | 5 + 8 files changed, 647 insertions(+), 706 deletions(-) create mode 100644 packages/react/src/screens/user-details-blocks/EmailsEdit.tsx create mode 100644 packages/react/src/screens/user-details-blocks/PhonesEdit.tsx create mode 100644 packages/react/src/screens/user-details-blocks/UserDelete.tsx create mode 100644 packages/react/src/screens/user-details-blocks/UsernameEdit.tsx create mode 100644 packages/react/src/util.ts diff --git a/packages/react/src/contexts/CorbadoUserDetailsContext.tsx b/packages/react/src/contexts/CorbadoUserDetailsContext.tsx index 15075b357..75f39359b 100644 --- a/packages/react/src/contexts/CorbadoUserDetailsContext.tsx +++ b/packages/react/src/contexts/CorbadoUserDetailsContext.tsx @@ -23,7 +23,7 @@ export interface CorbadoUserDetailsContextProps { emails: Identifier[] | undefined; setEmails: (email: Identifier[] | undefined) => void; phones: Identifier[] | undefined; - userNameEnabled: boolean; + usernameEnabled: boolean; emailEnabled: boolean; phoneEnabled: boolean; fullNameRequired: boolean; @@ -42,7 +42,7 @@ export const initialContext: CorbadoUserDetailsContextProps = { emails: undefined, setEmails: missingImplementation, phones: undefined, - userNameEnabled: false, + usernameEnabled: false, emailEnabled: false, phoneEnabled: false, fullNameRequired: false, diff --git a/packages/react/src/contexts/CorbadoUserDetailsProvider.tsx b/packages/react/src/contexts/CorbadoUserDetailsProvider.tsx index c7dc745cc..ecd678a0b 100644 --- a/packages/react/src/contexts/CorbadoUserDetailsProvider.tsx +++ b/packages/react/src/contexts/CorbadoUserDetailsProvider.tsx @@ -121,10 +121,10 @@ export const CorbadoUserDetailsProvider: FC = ({ children }) setEmails, phones, setPhones, - userNameEnabled: usernameEnabled, - emailEnabled: emailEnabled, - phoneEnabled: phoneEnabled, - fullNameRequired: fullNameRequired, + usernameEnabled, + emailEnabled, + phoneEnabled, + fullNameRequired, getCurrentUser, getConfig, }} diff --git a/packages/react/src/screens/core/UserDetails.tsx b/packages/react/src/screens/core/UserDetails.tsx index a4f88745f..140a32f08 100644 --- a/packages/react/src/screens/core/UserDetails.tsx +++ b/packages/react/src/screens/core/UserDetails.tsx @@ -1,376 +1,23 @@ -import { LoginIdentifierConfigType, LoginIdentifierType } from '@corbado/shared-ui'; -import { type CorbadoUser, type Identifier, type SocialAccount } from '@corbado/types'; import type { FC } from 'react'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, InputField, LoadingSpinner, PasskeyListErrorBoundary, Text } from '../../components'; -import { AddIcon } from '../../components/ui/icons/AddIcon'; -import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; -import { CopyIcon } from '../../components/ui/icons/CopyIcon'; -import { PendingIcon } from '../../components/ui/icons/PendingIcon'; -import { PrimaryIcon } from '../../components/ui/icons/PrimaryIcon'; -import { VerifiedIcon } from '../../components/ui/icons/VerifiedIcon'; -import { useCorbado } from '../../hooks/useCorbado'; - -interface ProcessedUser { - name: string; - username: string; - emails: Identifier[]; - phoneNumbers: Identifier[]; - socialAccounts: SocialAccount[]; -} +import { LoadingSpinner, PasskeyListErrorBoundary, Text } from '../../components'; +import { CorbadoUserDetailsProvider } from '../../contexts/CorbadoUserDetailsProvider'; +import { useCorbado } from '../..'; +import NameEdit from '../user-details-blocks/NameEdit'; +import UsernameEdit from '../user-details-blocks/UsernameEdit'; +import EmailsEdit from '../user-details-blocks/EmailsEdit'; +import PhonesEdit from '../user-details-blocks/PhonesEdit'; +import UserDelete from '../user-details-blocks/UserDelete'; export const UserDetails: FC = () => { - const { - corbadoApp, - isAuthenticated, - globalError, - getFullUser, - getIdentifierListConfig, - updateName, - updateUsername, - createIdentifier, - deleteIdentifier, - verifyIdentifierStart, - verifyIdentifierFinish, - deleteUser, - } = useCorbado(); + const { globalError, isAuthenticated, loading } = useCorbado(); const { t } = useTranslation('translation'); - const [currentUser, setCurrentUser] = useState(); - const [loading, setLoading] = useState(false); - - const [fullNameRequired, setFullNameRequired] = useState(false); - const [usernameEnabled, setUsernameEnabled] = useState(false); - const [emailEnabled, setEmailEnabled] = useState(false); - const [phoneEnabled, setPhoneEnabled] = useState(false); - - const [name, setName] = useState(); - const [editingName, setEditingName] = useState(false); - - const [username, setUsername] = useState(); - const [addingUsername, setAddingUsername] = useState(false); - const [editingUsername, setEditingUsername] = useState(false); - - const [emails, setEmails] = useState([]); - const [verifyingEmails, setVerifyingEmails] = useState([]); - const [emailChallengeCodes, setEmailChallengeCodes] = useState([]); - const [addingEmail, setAddingEmail] = useState(false); - const [newEmail, setNewEmail] = useState(''); - - const [phones, setPhones] = useState([]); - const [verifyingPhones, setVerifyingPhones] = useState([]); - const [phoneChallengeCodes, setPhoneChallengeCodes] = useState([]); - const [addingPhone, setAddingPhone] = useState(false); - const [newPhone, setNewPhone] = useState(''); - - useEffect(() => { - if (!isAuthenticated) { - return; - } - - const abortController = new AbortController(); - void getCurrentUser(abortController); - void getConfig(abortController); - - return () => { - abortController.abort(); - }; - }, [isAuthenticated]); const title = useMemo(() => t('user-details.title'), [t]); - const headerName = useMemo(() => t('user-details.name'), [t]); - const headerUsername = useMemo(() => t('user-details.username'), [t]); - const headerEmail = useMemo(() => t('user-details.email'), [t]); - const headerPhone = useMemo(() => t('user-details.phone'), [t]); // const headerSocial = useMemo(() => t('user-details.social'), [t]); - const headerDelete = useMemo(() => t('user-details.delete_account'), [t]); - - const textDelete = useMemo(() => t('user-details.delete_account_text'), [t]); - - const badgePrimary = useMemo(() => t('user-details.primary'), [t]); - const badgeVerified = useMemo(() => t('user-details.verified'), [t]); - const badgePending = useMemo(() => t('user-details.pending'), [t]); - - const buttonVerify = useMemo(() => t('user-details.verify'), [t]); - const buttonRemove = useMemo(() => t('user-details.remove'), [t]); - const buttonDelete = useMemo(() => t('user-details.delete'), [t]); - const buttonSave = useMemo(() => t('user-details.save'), [t]); - const buttonCancel = useMemo(() => t('user-details.cancel'), [t]); - const buttonChange = useMemo(() => t('user-details.change'), [t]); - const buttonAddName = useMemo(() => t('user-details.add_name'), [t]); - const buttonAddUsername = useMemo(() => t('user-details.add_username'), [t]); - const buttonAddEmail = useMemo(() => t('user-details.add_email'), [t]); - const buttonAddPhone = useMemo(() => t('user-details.add_phone'), [t]); - - const processUser = useMemo((): ProcessedUser => { - if (!currentUser) { - return { - name: '', - username: '', - emails: [], - phoneNumbers: [], - socialAccounts: [], - }; - } - - return { - name: currentUser.fullName, - username: currentUser.identifiers.find(id => id.type === 'username')?.value || '', - emails: currentUser.identifiers.filter(id => id.type === 'email'), - phoneNumbers: currentUser.identifiers.filter(id => id.type === 'phone'), - socialAccounts: currentUser.socialAccounts, - }; - }, [currentUser]); - - const getCurrentUser = useCallback( - async (abortController?: AbortController) => { - setLoading(true); - const result = await getFullUser(abortController); - if (result.err && result.val.ignore) { - return; - } - - if (!result || result?.err) { - throw new Error(result?.val.name); - } - - setCurrentUser(result.val); - setName(result.val.fullName || ''); - const usernameIdentifier = result.val.identifiers.find( - identifier => identifier.type == LoginIdentifierType.Username, - ); - setUsername(usernameIdentifier); - const emails = result.val.identifiers.filter(identifier => identifier.type == LoginIdentifierType.Email); - setEmails(emails); - setVerifyingEmails(emails.map(() => false)); - setEmailChallengeCodes(emails.map(() => '')); - const phones = result.val.identifiers.filter(identifier => identifier.type == LoginIdentifierType.Phone); - setPhones(phones); - setVerifyingPhones(phones.map(() => false)); - setPhoneChallengeCodes(phones.map(() => '')); - setLoading(false); - }, - [corbadoApp], - ); - - const getConfig = useCallback( - async (abortController?: AbortController) => { - setLoading(true); - const result = await getIdentifierListConfig(abortController); - if (result.err && result.val.ignore) { - return; - } - - if (!result || result?.err) { - throw new Error(result?.val.name); - } - - setFullNameRequired(result.val.fullNameRequired); - for (const identifierConfig of result.val.identifiers) { - if (identifierConfig.type === LoginIdentifierConfigType.Username) { - setUsernameEnabled(true); - } else if (identifierConfig.type === LoginIdentifierConfigType.Email) { - setEmailEnabled(true); - } else if (identifierConfig.type === LoginIdentifierConfigType.Phone) { - setPhoneEnabled(true); - } - } - setLoading(false); - }, - [corbadoApp], - ); - - const copyName = async () => { - if (name) { - await navigator.clipboard.writeText(name); - } - }; - - const changeName = async () => { - if (!name) { - console.error('name is empty'); - return; - } - const res = await updateName(name); - if (res.err) { - // no possible error code - console.error(res.val.message); - return; - } - setEditingName(false); - void getCurrentUser(); - }; - - const copyUsername = async () => { - await navigator.clipboard.writeText(username?.value || ''); - }; - - const addUsername = async () => { - if (!username || !username.value) { - console.error('username is empty'); - return; - } - const res = await createIdentifier(LoginIdentifierType.Username, username?.value || ''); - if (res.err) { - const code = getErrorCode(res.val.message); - if (code) { - // possible code: unsupported_identifier_type (but the current UI flow should prevent this, because unsupported types are not shown) - console.error(t(`errors.${code}`)); - } - return; - } - setAddingUsername(false); - void getCurrentUser(); - }; - - const changeUsername = async () => { - if (!username || !username.value) { - console.error('username is empty'); - return; - } - const res = await updateUsername(username.id, username.value); - if (res.err) { - // no possible error code - console.error(res.val.message); - return; - } - setEditingUsername(false); - void getCurrentUser(); - }; - - const addEmail = async () => { - if (!newEmail) { - console.error('email is empty'); - return; - } - const res = await createIdentifier(LoginIdentifierType.Email, newEmail); - if (res.err) { - const code = getErrorCode(res.val.message); - if (code) { - // possible code: unsupported_identifier_type (but the current UI flow should prevent this, because unsupported types are not shown) - console.error(t(`errors.${code}`)); - } - return; - } - setNewEmail(''); - setAddingEmail(false); - void getCurrentUser(); - }; - - const removeEmail = async (index: number) => { - const res = await deleteIdentifier(emails[index].id); - if (res.err) { - const code = getErrorCode(res.val.message); - if (code) { - // possible codes: no_remaining_identifier, no_remaining_verified_identifier - console.error(t(`errors.${code}`)); - } - return; - } - void getCurrentUser(); - }; - - const startEmailVerification = async (index: number) => { - const res = await verifyIdentifierStart(emails[index].id); - if (res.err) { - const code = getErrorCode(res.val.message); - if (code) { - // possible code: wait_before_retry - console.error(t(`errors.${code}`)); - } - return; - } - setVerifyingEmails(verifyingEmails.map((v, i) => (i === index ? true : v))); - }; - - const finishEmailVerification = async (index: number) => { - const res = await verifyIdentifierFinish(emails[index].id, emailChallengeCodes[index]); - if (res.err) { - const code = getErrorCode(res.val.message); - if (code) { - // possible code: invalid_challenge_solution_email-otp - console.error(t(`errors.${code}`)); - } - return; - } - void getCurrentUser(); - }; - - const addPhone = async () => { - if (!newPhone) { - console.error('phone is empty'); - return; - } - const res = await createIdentifier(LoginIdentifierType.Phone, newPhone); - if (res.err) { - const code = getErrorCode(res.val.message); - if (code) { - // possible code: unsupported_identifier_type (but the current UI flow should prevent this, because unsupported types are not shown) - console.error(t(`errors.${code}`)); - } - return; - } - setNewPhone(''); - setAddingPhone(false); - void getCurrentUser(); - }; - - const removePhone = async (index: number) => { - const res = await deleteIdentifier(phones[index].id); - if (res.err) { - const code = getErrorCode(res.val.message); - if (code) { - // possible codes: no_remaining_identifier, no_remaining_verified_identifier - console.error(t(`errors.${code}`)); - } - return; - } - void getCurrentUser(); - }; - - const startPhoneVerification = async (index: number) => { - const res = await verifyIdentifierStart(phones[index].id); - if (res.err) { - const code = getErrorCode(res.val.message); - if (code) { - // possible code: wait_before_retry - console.error(t(`errors.${code}`)); - } - return; - } - setVerifyingPhones(verifyingPhones.map((v, i) => (i === index ? true : v))); - }; - - const finishPhoneVerification = async (index: number) => { - const res = await verifyIdentifierFinish(phones[index].id, phoneChallengeCodes[index]); - if (res.err) { - const code = getErrorCode(res.val.message); - if (code) { - // possible code: invalid_challenge_solution_phone-otp - console.error(t(`errors.${code}`)); - } - return; - } - void getCurrentUser(); - }; - - const deleteAccount = async () => { - const res = await deleteUser(); - if (res.err) { - // no possible error code - console.error(res.val.message); - return; - } - // TODO: go back to login page? - }; - - const getErrorCode = (message: string) => { - const regex = /\(([^)]+)\)/; - const matches = regex.exec(message); - return matches ? matches[1] : undefined; - }; if (!isAuthenticated) { return
{t('user-details.warning_notLoggedIn')}
; @@ -381,328 +28,16 @@ export const UserDetails: FC = () => { } return ( - -
- {title} - - {usernameEnabled && ( -
- {headerUsername} -
- {!processUser.username ? ( -
- {addingUsername ? ( -
-
- - setUsername({ id: '', type: 'username', status: 'verified', value: e.target.value }) - } - /> - void copyUsername()} - /> -
- - -
- ) : ( - - )} -
- ) : ( -
- {username && ( -
-
- setUsername({ ...username, value: e.target.value })} - /> - void copyUsername()} - /> -
- {editingUsername ? ( -
- - -
- ) : ( - - )} -
- )} -
- )} -
-
- )} - {emailEnabled && ( -
- {headerEmail} -
- {emails.map((email, index) => ( -
- {verifyingEmails[index] ? ( -
- Enter OTP code for: {email.value} - - setEmailChallengeCodes(emailChallengeCodes.map((c, i) => (i === index ? e.target.value : c))) - } - /> - - -
- ) : ( -
- {email.value} -
- {email.status === 'primary' ? ( -
- - {badgePrimary} -
- ) : email.status === 'verified' ? ( -
- - {badgeVerified} -
- ) : ( -
- - {badgePending} -
- )} -
- {email.status === 'pending' && ( - - )} - -
- )} -
- ))} - {addingEmail ? ( -
- setNewEmail(e.target.value)} - /> - - -
- ) : ( - - )} -
-
- )} - {phoneEnabled && ( -
- {headerPhone} -
- {phones.map((phone, index) => ( -
- {verifyingPhones[index] ? ( -
- Enter OTP code for: {phone.value} - - setPhoneChallengeCodes(phoneChallengeCodes.map((c, i) => (i === index ? e.target.value : c))) - } - /> - - -
- ) : ( -
- {phone.value} -
- {phone.status === 'primary' ? ( -
- - {badgePrimary} -
- ) : phone.status === 'verified' ? ( -
- - {badgeVerified} -
- ) : ( -
- - {badgePending} -
- )} -
- {phone.status === 'pending' && ( - - )} - -
- )} -
- ))} - {addingPhone ? ( -
- setNewPhone(e.target.value)} - /> - - -
- ) : ( - - )} -
-
- )} - - {/*
+ + +
+ {title} + + + + + + {/*
{processUser.socialAccounts.map((social, i) => (
{ ))}
*/} -
- {headerDelete} -
-
- {textDelete} -
-
- +
-
- + + ); }; diff --git a/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx b/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx new file mode 100644 index 000000000..12b252349 --- /dev/null +++ b/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx @@ -0,0 +1,202 @@ +import React, { useMemo, useState } from 'react'; +import { useCorbado } from '../../hooks/useCorbado'; +import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; +import { LoginIdentifierType } from '@corbado/types'; +import { getErrorCode } from '../../util'; +import { t } from 'i18next'; +import { InputField, Button, Text } from '../../components'; +import { AddIcon } from '../../components/ui/icons/AddIcon'; +import { PrimaryIcon } from '../../components/ui/icons/PrimaryIcon'; +import { VerifiedIcon } from '../../components/ui/icons/VerifiedIcon'; +import { PendingIcon } from '../../components/ui/icons/PendingIcon'; + +const EmailsEdit = () => { + const { createIdentifier, verifyIdentifierStart, verifyIdentifierFinish, deleteIdentifier } = useCorbado(); + const { emails = [], getCurrentUser, emailEnabled } = useCorbadoUserDetails(); + + const [verifyingEmails, setVerifyingEmails] = useState([]); + const [emailChallengeCodes, setEmailChallengeCodes] = useState([]); + const [addingEmail, setAddingEmail] = useState(false); + const [newEmail, setNewEmail] = useState(''); + + const headerEmail = useMemo(() => t('user-details.email'), [t]); + + const badgePrimary = useMemo(() => t('user-details.primary'), [t]); + const badgeVerified = useMemo(() => t('user-details.verified'), [t]); + const badgePending = useMemo(() => t('user-details.pending'), [t]); + + const buttonSave = useMemo(() => t('user-details.save'), [t]); + const buttonCancel = useMemo(() => t('user-details.cancel'), [t]); + const buttonAddEmail = useMemo(() => t('user-details.add_email'), [t]); + const buttonVerify = useMemo(() => t('user-details.verify'), [t]); + const buttonRemove = useMemo(() => t('user-details.remove'), [t]); + + const addEmail = async () => { + if (!newEmail) { + console.error('email is empty'); + return; + } + const res = await createIdentifier(LoginIdentifierType.Email, newEmail); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible code: unsupported_identifier_type (but the current UI flow should prevent this, because unsupported types are not shown) + console.error(t(`errors.${code}`)); + } + return; + } + setNewEmail(''); + setAddingEmail(false); + void getCurrentUser(); + }; + + const removeEmail = async (index: number) => { + const res = await deleteIdentifier(emails[index].id); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible codes: no_remaining_identifier, no_remaining_verified_identifier + console.error(t(`errors.${code}`)); + } + return; + } + void getCurrentUser(); + }; + + const startEmailVerification = async (index: number) => { + const res = await verifyIdentifierStart(emails[index].id); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible code: wait_before_retry + console.error(t(`errors.${code}`)); + } + return; + } + setVerifyingEmails(verifyingEmails.map((v, i) => (i === index ? true : v))); + }; + + const finishEmailVerification = async (index: number) => { + const res = await verifyIdentifierFinish(emails[index].id, emailChallengeCodes[index]); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible code: invalid_challenge_solution_email-otp + console.error(t(`errors.${code}`)); + } + return; + } + void getCurrentUser(); + }; + + if (!emailEnabled) return null; + + return ( +
+ {headerEmail} +
+ {emails.map((email, index) => ( +
+ {verifyingEmails[index] ? ( +
+ Enter OTP code for: {email.value} + + setEmailChallengeCodes(emailChallengeCodes.map((c, i) => (i === index ? e.target.value : c))) + } + /> + + +
+ ) : ( +
+ {email.value} +
+ {email.status === 'primary' ? ( +
+ + {badgePrimary} +
+ ) : email.status === 'verified' ? ( +
+ + {badgeVerified} +
+ ) : ( +
+ + {badgePending} +
+ )} +
+ {email.status === 'pending' && ( + + )} + +
+ )} +
+ ))} + {addingEmail ? ( +
+ setNewEmail(e.target.value)} + /> + + +
+ ) : ( + + )} +
+
+ ); +}; + +export default EmailsEdit; diff --git a/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx b/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx new file mode 100644 index 000000000..3026b1216 --- /dev/null +++ b/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx @@ -0,0 +1,202 @@ +import React, { useMemo, useState } from 'react'; +import { useCorbado } from '../../hooks/useCorbado'; +import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; +import { LoginIdentifierType } from '@corbado/types'; +import { getErrorCode } from '../../util'; +import { t } from 'i18next'; +import { InputField, Button, Text } from '../../components'; +import { AddIcon } from '../../components/ui/icons/AddIcon'; +import { PrimaryIcon } from '../../components/ui/icons/PrimaryIcon'; +import { VerifiedIcon } from '../../components/ui/icons/VerifiedIcon'; +import { PendingIcon } from '../../components/ui/icons/PendingIcon'; + +const PhonesEdit = () => { + const { createIdentifier, verifyIdentifierStart, verifyIdentifierFinish, deleteIdentifier } = useCorbado(); + const { phones = [], getCurrentUser, phoneEnabled } = useCorbadoUserDetails(); + + const [verifyingPhones, setVerifyingPhones] = useState([]); + const [phoneChallengeCodes, setPhoneChallengeCodes] = useState([]); + const [addingPhone, setAddingPhone] = useState(false); + const [newPhone, setNewPhone] = useState(''); + + const headerPhone = useMemo(() => t('user-details.phone'), [t]); + + const badgePrimary = useMemo(() => t('user-details.primary'), [t]); + const badgeVerified = useMemo(() => t('user-details.verified'), [t]); + const badgePending = useMemo(() => t('user-details.pending'), [t]); + + const buttonSave = useMemo(() => t('user-details.save'), [t]); + const buttonCancel = useMemo(() => t('user-details.cancel'), [t]); + const buttonAddPhone = useMemo(() => t('user-details.add_phone'), [t]); + const buttonVerify = useMemo(() => t('user-details.verify'), [t]); + const buttonRemove = useMemo(() => t('user-details.remove'), [t]); + + const addPhone = async () => { + if (!newPhone) { + console.error('phone is empty'); + return; + } + const res = await createIdentifier(LoginIdentifierType.Phone, newPhone); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible code: unsupported_identifier_type (but the current UI flow should prevent this, because unsupported types are not shown) + console.error(t(`errors.${code}`)); + } + return; + } + setNewPhone(''); + setAddingPhone(false); + void getCurrentUser(); + }; + + const removePhone = async (index: number) => { + const res = await deleteIdentifier(phones[index].id); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible codes: no_remaining_identifier, no_remaining_verified_identifier + console.error(t(`errors.${code}`)); + } + return; + } + void getCurrentUser(); + }; + + const startPhoneVerification = async (index: number) => { + const res = await verifyIdentifierStart(phones[index].id); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible code: wait_before_retry + console.error(t(`errors.${code}`)); + } + return; + } + setVerifyingPhones(verifyingPhones.map((v, i) => (i === index ? true : v))); + }; + + const finishPhoneVerification = async (index: number) => { + const res = await verifyIdentifierFinish(phones[index].id, phoneChallengeCodes[index]); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible code: invalid_challenge_solution_phone-otp + console.error(t(`errors.${code}`)); + } + return; + } + void getCurrentUser(); + }; + + if (!phoneEnabled) return null; + + return ( +
+ {headerPhone} +
+ {phones.map((phone, index) => ( +
+ {verifyingPhones[index] ? ( +
+ Enter OTP code for: {phone.value} + + setPhoneChallengeCodes(phoneChallengeCodes.map((c, i) => (i === index ? e.target.value : c))) + } + /> + + +
+ ) : ( +
+ {phone.value} +
+ {phone.status === 'primary' ? ( +
+ + {badgePrimary} +
+ ) : phone.status === 'verified' ? ( +
+ + {badgeVerified} +
+ ) : ( +
+ + {badgePending} +
+ )} +
+ {phone.status === 'pending' && ( + + )} + +
+ )} +
+ ))} + {addingPhone ? ( +
+ setNewPhone(e.target.value)} + /> + + +
+ ) : ( + + )} +
+
+ ); +}; + +export default PhonesEdit; diff --git a/packages/react/src/screens/user-details-blocks/UserDelete.tsx b/packages/react/src/screens/user-details-blocks/UserDelete.tsx new file mode 100644 index 000000000..4ddab2bf5 --- /dev/null +++ b/packages/react/src/screens/user-details-blocks/UserDelete.tsx @@ -0,0 +1,41 @@ +import React, { useMemo } from 'react'; +import { Button, Text } from '../../components'; +import { t } from 'i18next'; +import { useCorbado } from '../../hooks/useCorbado'; + +const UserDelete = () => { + const { deleteUser } = useCorbado(); + + const headerDelete = useMemo(() => t('user-details.delete_account'), [t]); + const buttonDelete = useMemo(() => t('user-details.delete'), [t]); + const textDelete = useMemo(() => t('user-details.delete_account_text'), [t]); + + const deleteAccount = async () => { + const res = await deleteUser(); + if (res.err) { + // no possible error code + console.error(res.val.message); + return; + } + // TODO: go back to login page? + }; + + return ( +
+ {headerDelete} +
+
+ {textDelete} +
+
+ +
+ ); +}; + +export default UserDelete; diff --git a/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx b/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx new file mode 100644 index 000000000..915a46f2e --- /dev/null +++ b/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx @@ -0,0 +1,168 @@ +import React, { useMemo, useState } from 'react'; +import { useCorbado } from '../../hooks/useCorbado'; +import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; +import { LoginIdentifierType } from '@corbado/types'; +import { getErrorCode } from '../../util'; +import { t } from 'i18next'; +import { InputField, Button, Text } from '../../components'; +import { AddIcon } from '../../components/ui/icons/AddIcon'; +import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; +import { CopyIcon } from '../../components/ui/icons/CopyIcon'; + +const UsernameEdit = () => { + const { createIdentifier, updateUsername } = useCorbado(); + const { username, getCurrentUser, setUsername, processUser, usernameEnabled } = useCorbadoUserDetails(); + + const [addingUsername, setAddingUsername] = useState(false); + const [editingUsername, setEditingUsername] = useState(false); + + const headerUsername = useMemo(() => t('user-details.username'), [t]); + const buttonSave = useMemo(() => t('user-details.save'), [t]); + const buttonCancel = useMemo(() => t('user-details.cancel'), [t]); + const buttonChange = useMemo(() => t('user-details.change'), [t]); + const buttonAddUsername = useMemo(() => t('user-details.add_username'), [t]); + + const copyUsername = async () => { + await navigator.clipboard.writeText(username?.value || ''); + }; + + const addUsername = async () => { + if (!username || !username.value) { + console.error('username is empty'); + return; + } + const res = await createIdentifier(LoginIdentifierType.Username, username?.value || ''); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible code: unsupported_identifier_type (but the current UI flow should prevent this, because unsupported types are not shown) + console.error(t(`errors.${code}`)); + } + return; + } + setAddingUsername(false); + void getCurrentUser(); + }; + + const changeUsername = async () => { + if (!username || !username.value) { + console.error('username is empty'); + return; + } + const res = await updateUsername(username.id, username.value); + if (res.err) { + // no possible error code + console.error(res.val.message); + return; + } + setEditingUsername(false); + void getCurrentUser(); + }; + + if (!processUser || !usernameEnabled) return; + + return ( +
+ {headerUsername} +
+ {!processUser.username ? ( +
+ {addingUsername ? ( +
+
+ setUsername({ id: '', type: 'username', status: 'verified', value: e.target.value })} + /> + void copyUsername()} + /> +
+ + +
+ ) : ( + + )} +
+ ) : ( +
+ {username && ( +
+
+ setUsername({ ...username, value: e.target.value })} + /> + void copyUsername()} + /> +
+ {editingUsername ? ( +
+ + +
+ ) : ( + + )} +
+ )} +
+ )} +
+
+ ); +}; + +export default UsernameEdit; diff --git a/packages/react/src/util.ts b/packages/react/src/util.ts new file mode 100644 index 000000000..cd0e82dff --- /dev/null +++ b/packages/react/src/util.ts @@ -0,0 +1,5 @@ +export const getErrorCode = (message: string) => { + const regex = /\(([^)]+)\)/; + const matches = regex.exec(message); + return matches ? matches[1] : undefined; +}; From 0d37c34c5f70a362e0d7a6bca496e955b6d4f5f3 Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Fri, 16 Aug 2024 11:28:37 +0200 Subject: [PATCH 21/55] fixes build issues --- packages/react/src/contexts/CorbadoUserDetailsProvider.tsx | 3 ++- packages/react/src/screens/user-details-blocks/EmailsEdit.tsx | 2 +- packages/react/src/screens/user-details-blocks/NameEdit.tsx | 2 +- packages/react/src/screens/user-details-blocks/PhonesEdit.tsx | 2 +- .../react/src/screens/user-details-blocks/UsernameEdit.tsx | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/react/src/contexts/CorbadoUserDetailsProvider.tsx b/packages/react/src/contexts/CorbadoUserDetailsProvider.tsx index ecd678a0b..b76909120 100644 --- a/packages/react/src/contexts/CorbadoUserDetailsProvider.tsx +++ b/packages/react/src/contexts/CorbadoUserDetailsProvider.tsx @@ -1,7 +1,8 @@ import type { FC, PropsWithChildren } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { CorbadoUserDetailsContext } from './CorbadoUserDetailsContext'; -import { CorbadoUser, Identifier, LoginIdentifierConfigType, LoginIdentifierType } from '@corbado/types'; +import { LoginIdentifierConfigType, LoginIdentifierType } from '@corbado/shared-ui'; +import type { CorbadoUser, Identifier } from '@corbado/types'; import { useCorbado } from '../hooks/useCorbado'; export const CorbadoUserDetailsProvider: FC = ({ children }) => { diff --git a/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx b/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx index 12b252349..e48639390 100644 --- a/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useState } from 'react'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; -import { LoginIdentifierType } from '@corbado/types'; +import { LoginIdentifierType } from '@corbado/shared-ui'; import { getErrorCode } from '../../util'; import { t } from 'i18next'; import { InputField, Button, Text } from '../../components'; diff --git a/packages/react/src/screens/user-details-blocks/NameEdit.tsx b/packages/react/src/screens/user-details-blocks/NameEdit.tsx index 3ff016e5c..d31fa92e1 100644 --- a/packages/react/src/screens/user-details-blocks/NameEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/NameEdit.tsx @@ -5,7 +5,7 @@ import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; import { CopyIcon } from '../../components/ui/icons/CopyIcon'; import { useTranslation } from 'react-i18next'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; -import { useCorbado } from '../..'; +import { useCorbado } from '../../hooks/useCorbado'; const NameEdit: FC = () => { const { updateName } = useCorbado(); diff --git a/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx b/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx index 3026b1216..5d7c833b6 100644 --- a/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useState } from 'react'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; -import { LoginIdentifierType } from '@corbado/types'; +import { LoginIdentifierType } from '@corbado/shared-ui'; import { getErrorCode } from '../../util'; import { t } from 'i18next'; import { InputField, Button, Text } from '../../components'; diff --git a/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx b/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx index 915a46f2e..db9870ca6 100644 --- a/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useState } from 'react'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; -import { LoginIdentifierType } from '@corbado/types'; +import { LoginIdentifierType } from '@corbado/shared-ui'; import { getErrorCode } from '../../util'; import { t } from 'i18next'; import { InputField, Button, Text } from '../../components'; From 20736eb0897b39bcf44548e0f26d48530341a7d3 Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Fri, 16 Aug 2024 11:33:24 +0200 Subject: [PATCH 22/55] fixes lint issues --- .../src/contexts/CorbadoUserDetailsContext.tsx | 2 +- .../src/contexts/CorbadoUserDetailsProvider.tsx | 7 ++++--- packages/react/src/hooks/useCorbadoUserDetails.ts | 3 ++- packages/react/src/screens/core/UserDetails.tsx | 6 +++--- .../screens/user-details-blocks/EmailsEdit.tsx | 15 ++++++++------- .../src/screens/user-details-blocks/NameEdit.tsx | 10 ++++++---- .../screens/user-details-blocks/PhonesEdit.tsx | 15 ++++++++------- .../screens/user-details-blocks/UserDelete.tsx | 3 ++- .../screens/user-details-blocks/UsernameEdit.tsx | 13 +++++++------ 9 files changed, 41 insertions(+), 33 deletions(-) diff --git a/packages/react/src/contexts/CorbadoUserDetailsContext.tsx b/packages/react/src/contexts/CorbadoUserDetailsContext.tsx index 75f39359b..8bce90a17 100644 --- a/packages/react/src/contexts/CorbadoUserDetailsContext.tsx +++ b/packages/react/src/contexts/CorbadoUserDetailsContext.tsx @@ -1,5 +1,5 @@ +import type { Identifier, SocialAccount } from '@corbado/types'; import { createContext } from 'react'; -import { Identifier, SocialAccount } from '@corbado/types'; const missingImplementation = (): never => { throw new Error('Please make sure that your components are wrapped inside '); diff --git a/packages/react/src/contexts/CorbadoUserDetailsProvider.tsx b/packages/react/src/contexts/CorbadoUserDetailsProvider.tsx index b76909120..dce80b4dd 100644 --- a/packages/react/src/contexts/CorbadoUserDetailsProvider.tsx +++ b/packages/react/src/contexts/CorbadoUserDetailsProvider.tsx @@ -1,9 +1,10 @@ -import type { FC, PropsWithChildren } from 'react'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { CorbadoUserDetailsContext } from './CorbadoUserDetailsContext'; import { LoginIdentifierConfigType, LoginIdentifierType } from '@corbado/shared-ui'; import type { CorbadoUser, Identifier } from '@corbado/types'; +import type { FC, PropsWithChildren } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + import { useCorbado } from '../hooks/useCorbado'; +import { CorbadoUserDetailsContext } from './CorbadoUserDetailsContext'; export const CorbadoUserDetailsProvider: FC = ({ children }) => { const { corbadoApp, isAuthenticated, getFullUser, getIdentifierListConfig } = useCorbado(); diff --git a/packages/react/src/hooks/useCorbadoUserDetails.ts b/packages/react/src/hooks/useCorbadoUserDetails.ts index 76913b6ac..fec40753b 100644 --- a/packages/react/src/hooks/useCorbadoUserDetails.ts +++ b/packages/react/src/hooks/useCorbadoUserDetails.ts @@ -1,6 +1,7 @@ import { useContext } from 'react'; -import { CorbadoUserDetailsContext, CorbadoUserDetailsContextProps } from '../contexts/CorbadoUserDetailsContext'; +import type { CorbadoUserDetailsContextProps } from '../contexts/CorbadoUserDetailsContext'; +import { CorbadoUserDetailsContext } from '../contexts/CorbadoUserDetailsContext'; export const useCorbadoUserDetails = (): CorbadoUserDetailsContextProps => { const corbadoUserDetails = useContext(CorbadoUserDetailsContext); diff --git a/packages/react/src/screens/core/UserDetails.tsx b/packages/react/src/screens/core/UserDetails.tsx index 140a32f08..5fd4ec8f3 100644 --- a/packages/react/src/screens/core/UserDetails.tsx +++ b/packages/react/src/screens/core/UserDetails.tsx @@ -2,14 +2,14 @@ import type { FC } from 'react'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useCorbado } from '../..'; import { LoadingSpinner, PasskeyListErrorBoundary, Text } from '../../components'; import { CorbadoUserDetailsProvider } from '../../contexts/CorbadoUserDetailsProvider'; -import { useCorbado } from '../..'; -import NameEdit from '../user-details-blocks/NameEdit'; -import UsernameEdit from '../user-details-blocks/UsernameEdit'; import EmailsEdit from '../user-details-blocks/EmailsEdit'; +import NameEdit from '../user-details-blocks/NameEdit'; import PhonesEdit from '../user-details-blocks/PhonesEdit'; import UserDelete from '../user-details-blocks/UserDelete'; +import UsernameEdit from '../user-details-blocks/UsernameEdit'; export const UserDetails: FC = () => { const { globalError, isAuthenticated, loading } = useCorbado(); diff --git a/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx b/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx index e48639390..4d0de2315 100644 --- a/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx @@ -1,14 +1,15 @@ -import React, { useMemo, useState } from 'react'; -import { useCorbado } from '../../hooks/useCorbado'; -import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { LoginIdentifierType } from '@corbado/shared-ui'; -import { getErrorCode } from '../../util'; import { t } from 'i18next'; -import { InputField, Button, Text } from '../../components'; +import React, { useMemo, useState } from 'react'; + +import { Button, InputField, Text } from '../../components'; import { AddIcon } from '../../components/ui/icons/AddIcon'; +import { PendingIcon } from '../../components/ui/icons/PendingIcon'; import { PrimaryIcon } from '../../components/ui/icons/PrimaryIcon'; import { VerifiedIcon } from '../../components/ui/icons/VerifiedIcon'; -import { PendingIcon } from '../../components/ui/icons/PendingIcon'; +import { useCorbado } from '../../hooks/useCorbado'; +import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; +import { getErrorCode } from '../../util'; const EmailsEdit = () => { const { createIdentifier, verifyIdentifierStart, verifyIdentifierFinish, deleteIdentifier } = useCorbado(); @@ -89,7 +90,7 @@ const EmailsEdit = () => { void getCurrentUser(); }; - if (!emailEnabled) return null; + if (!emailEnabled) {return null;} return (
diff --git a/packages/react/src/screens/user-details-blocks/NameEdit.tsx b/packages/react/src/screens/user-details-blocks/NameEdit.tsx index d31fa92e1..fe1c3ce1d 100644 --- a/packages/react/src/screens/user-details-blocks/NameEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/NameEdit.tsx @@ -1,11 +1,13 @@ -import React, { FC, useMemo, useState } from 'react'; +import type { FC} from 'react'; +import React, { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + import { Button, InputField, Text } from '../../components'; import { AddIcon } from '../../components/ui/icons/AddIcon'; import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; import { CopyIcon } from '../../components/ui/icons/CopyIcon'; -import { useTranslation } from 'react-i18next'; -import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { useCorbado } from '../../hooks/useCorbado'; +import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; const NameEdit: FC = () => { const { updateName } = useCorbado(); @@ -42,7 +44,7 @@ const NameEdit: FC = () => { void getCurrentUser(); }; - if (!processUser || !fullNameRequired) return; + if (!processUser || !fullNameRequired) {return;} return (
diff --git a/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx b/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx index 5d7c833b6..9efd51640 100644 --- a/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx @@ -1,14 +1,15 @@ -import React, { useMemo, useState } from 'react'; -import { useCorbado } from '../../hooks/useCorbado'; -import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { LoginIdentifierType } from '@corbado/shared-ui'; -import { getErrorCode } from '../../util'; import { t } from 'i18next'; -import { InputField, Button, Text } from '../../components'; +import React, { useMemo, useState } from 'react'; + +import { Button, InputField, Text } from '../../components'; import { AddIcon } from '../../components/ui/icons/AddIcon'; +import { PendingIcon } from '../../components/ui/icons/PendingIcon'; import { PrimaryIcon } from '../../components/ui/icons/PrimaryIcon'; import { VerifiedIcon } from '../../components/ui/icons/VerifiedIcon'; -import { PendingIcon } from '../../components/ui/icons/PendingIcon'; +import { useCorbado } from '../../hooks/useCorbado'; +import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; +import { getErrorCode } from '../../util'; const PhonesEdit = () => { const { createIdentifier, verifyIdentifierStart, verifyIdentifierFinish, deleteIdentifier } = useCorbado(); @@ -89,7 +90,7 @@ const PhonesEdit = () => { void getCurrentUser(); }; - if (!phoneEnabled) return null; + if (!phoneEnabled) {return null;} return (
diff --git a/packages/react/src/screens/user-details-blocks/UserDelete.tsx b/packages/react/src/screens/user-details-blocks/UserDelete.tsx index 4ddab2bf5..d7ab07fc6 100644 --- a/packages/react/src/screens/user-details-blocks/UserDelete.tsx +++ b/packages/react/src/screens/user-details-blocks/UserDelete.tsx @@ -1,6 +1,7 @@ +import { t } from 'i18next'; import React, { useMemo } from 'react'; + import { Button, Text } from '../../components'; -import { t } from 'i18next'; import { useCorbado } from '../../hooks/useCorbado'; const UserDelete = () => { diff --git a/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx b/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx index db9870ca6..0f60ca19f 100644 --- a/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx @@ -1,13 +1,14 @@ -import React, { useMemo, useState } from 'react'; -import { useCorbado } from '../../hooks/useCorbado'; -import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { LoginIdentifierType } from '@corbado/shared-ui'; -import { getErrorCode } from '../../util'; import { t } from 'i18next'; -import { InputField, Button, Text } from '../../components'; +import React, { useMemo, useState } from 'react'; + +import { Button, InputField, Text } from '../../components'; import { AddIcon } from '../../components/ui/icons/AddIcon'; import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; import { CopyIcon } from '../../components/ui/icons/CopyIcon'; +import { useCorbado } from '../../hooks/useCorbado'; +import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; +import { getErrorCode } from '../../util'; const UsernameEdit = () => { const { createIdentifier, updateUsername } = useCorbado(); @@ -59,7 +60,7 @@ const UsernameEdit = () => { void getCurrentUser(); }; - if (!processUser || !usernameEnabled) return; + if (!processUser || !usernameEnabled) {return;} return (
From 1f867f4230fbbd359eafd1af8caf9a4b9953bdea Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Fri, 16 Aug 2024 11:43:47 +0200 Subject: [PATCH 23/55] fixes formatting issues --- .../react/src/screens/user-details-blocks/EmailsEdit.tsx | 4 +++- packages/react/src/screens/user-details-blocks/NameEdit.tsx | 6 ++++-- .../react/src/screens/user-details-blocks/PhonesEdit.tsx | 4 +++- .../react/src/screens/user-details-blocks/UsernameEdit.tsx | 4 +++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx b/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx index 4d0de2315..04d9719a2 100644 --- a/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx @@ -90,7 +90,9 @@ const EmailsEdit = () => { void getCurrentUser(); }; - if (!emailEnabled) {return null;} + if (!emailEnabled) { + return null; + } return (
diff --git a/packages/react/src/screens/user-details-blocks/NameEdit.tsx b/packages/react/src/screens/user-details-blocks/NameEdit.tsx index fe1c3ce1d..9ded00470 100644 --- a/packages/react/src/screens/user-details-blocks/NameEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/NameEdit.tsx @@ -1,4 +1,4 @@ -import type { FC} from 'react'; +import type { FC } from 'react'; import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -44,7 +44,9 @@ const NameEdit: FC = () => { void getCurrentUser(); }; - if (!processUser || !fullNameRequired) {return;} + if (!processUser || !fullNameRequired) { + return; + } return (
diff --git a/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx b/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx index 9efd51640..b619da311 100644 --- a/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx @@ -90,7 +90,9 @@ const PhonesEdit = () => { void getCurrentUser(); }; - if (!phoneEnabled) {return null;} + if (!phoneEnabled) { + return null; + } return (
diff --git a/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx b/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx index 0f60ca19f..649f89c0b 100644 --- a/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx @@ -60,7 +60,9 @@ const UsernameEdit = () => { void getCurrentUser(); }; - if (!processUser || !usernameEnabled) {return;} + if (!processUser || !usernameEnabled) { + return; + } return (
From 4eaf0ca0df6e03533c639f8d8c2a83e03b56343a Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Fri, 16 Aug 2024 14:58:50 +0200 Subject: [PATCH 24/55] finish user deletion block --- package-lock.json | 15 +- .../src/components/ui/icons/AlertIcon.tsx | 38 ++++ .../src/components/user-details/Alert.tsx | 21 ++ .../user-details/UserDetailsCard.tsx | 18 ++ .../user-details-blocks/EmailsEdit.tsx | 195 +++++++++--------- .../screens/user-details-blocks/NameEdit.tsx | 110 +++++----- .../user-details-blocks/PhonesEdit.tsx | 195 +++++++++--------- .../user-details-blocks/UserDelete.tsx | 58 ++++-- .../user-details-blocks/UsernameEdit.tsx | 182 ++++++++-------- packages/shared-ui/src/assets/alert.svg | 4 + packages/shared-ui/src/i18n/en.json | 3 + packages/shared-ui/src/index.ts | 2 + packages/shared-ui/src/styles/typography.css | 4 + .../shared-ui/src/styles/user-details.css | 44 +++- 14 files changed, 516 insertions(+), 373 deletions(-) create mode 100644 packages/react/src/components/ui/icons/AlertIcon.tsx create mode 100644 packages/react/src/components/user-details/Alert.tsx create mode 100644 packages/react/src/components/user-details/UserDetailsCard.tsx create mode 100644 packages/shared-ui/src/assets/alert.svg diff --git a/package-lock.json b/package-lock.json index 9a2775dbd..143c1d73f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4027,13 +4027,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@fingerprintjs/fingerprintjs": { - "version": "3.4.2", - "license": "MIT", - "dependencies": { - "tslib": "^2.4.1" - } - }, "node_modules/@github/webauthn-json": { "version": "2.1.1", "license": "MIT", @@ -5836,6 +5829,7 @@ "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", "integrity": "sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.3.1" } @@ -24781,6 +24775,13 @@ "user-agent-data-types": "^0.4.2" } }, + "packages/web-core/node_modules/@fingerprintjs/fingerprintjs": { + "version": "3.4.2", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.1" + } + }, "packages/web-js": { "name": "@corbado/web-js", "version": "2.12.1", diff --git a/packages/react/src/components/ui/icons/AlertIcon.tsx b/packages/react/src/components/ui/icons/AlertIcon.tsx new file mode 100644 index 000000000..b39d7c225 --- /dev/null +++ b/packages/react/src/components/ui/icons/AlertIcon.tsx @@ -0,0 +1,38 @@ +import alertSrc from '@corbado/shared-ui/assets/alert.svg'; +import type { FC } from 'react'; +import { useRef } from 'react'; +import React from 'react'; + +import { useIconWithTheme } from '../../../hooks/useIconWithTheme'; +import type { IconProps } from './Icon'; +import { Icon } from './Icon'; + +export interface AlertIconProps extends IconProps { + color?: 'primary' | 'secondary' | 'error'; +} + +export const AlertIcon: FC = ({ color, ...props }) => { + const svgRef = useRef(null); + + const getColor = () => { + switch (color) { + case 'secondary': + return '--cb-text-primary-color'; + case 'error': + return '--cb-error-text-color'; + default: + return '--cb-button-text-primary'; + } + }; + + const { logoSVG } = useIconWithTheme(svgRef, alertSrc, getColor()); + + return ( + + ); +}; diff --git a/packages/react/src/components/user-details/Alert.tsx b/packages/react/src/components/user-details/Alert.tsx new file mode 100644 index 000000000..9adf24a94 --- /dev/null +++ b/packages/react/src/components/user-details/Alert.tsx @@ -0,0 +1,21 @@ +import React, { FC } from 'react'; +import { Text } from '../ui'; +import { AlertIcon } from '../ui/icons/AlertIcon'; + +interface Props { + text: string; +} + +const Alert: FC = ({ text }) => { + return ( +
+ + {text} +
+ ); +}; + +export default Alert; diff --git a/packages/react/src/components/user-details/UserDetailsCard.tsx b/packages/react/src/components/user-details/UserDetailsCard.tsx new file mode 100644 index 000000000..c7cea20c6 --- /dev/null +++ b/packages/react/src/components/user-details/UserDetailsCard.tsx @@ -0,0 +1,18 @@ +import type { FC, PropsWithChildren } from 'react'; +import { Text } from '../ui'; +import React from 'react'; + +interface Props extends PropsWithChildren { + header: string; +} + +const UserDetailsCard: FC = ({ header, children }) => { + return ( +
+ {header} +
{children}
+
+ ); +}; + +export default UserDetailsCard; diff --git a/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx b/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx index 04d9719a2..cfaf226e8 100644 --- a/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/EmailsEdit.tsx @@ -10,6 +10,7 @@ import { VerifiedIcon } from '../../components/ui/icons/VerifiedIcon'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { getErrorCode } from '../../util'; +import UserDetailsCard from '../../components/user-details/UserDetailsCard'; const EmailsEdit = () => { const { createIdentifier, verifyIdentifierStart, verifyIdentifierFinish, deleteIdentifier } = useCorbado(); @@ -95,110 +96,110 @@ const EmailsEdit = () => { } return ( -
- {headerEmail} -
- {emails.map((email, index) => ( -
- {verifyingEmails[index] ? ( -
- Enter OTP code for: {email.value} - - setEmailChallengeCodes(emailChallengeCodes.map((c, i) => (i === index ? e.target.value : c))) - } - /> - - -
- ) : ( -
- {email.value} -
- {email.status === 'primary' ? ( -
- - {badgePrimary} -
- ) : email.status === 'verified' ? ( -
- - {badgeVerified} -
- ) : ( -
- - {badgePending} -
- )} -
- {email.status === 'pending' && ( - + + {emails.map((email, index) => ( +
+ {verifyingEmails[index] ? ( +
+ Enter OTP code for: {email.value} + + setEmailChallengeCodes(emailChallengeCodes.map((c, i) => (i === index ? e.target.value : c))) + } + /> + + +
+ ) : ( +
+ {email.value} +
+ {email.status === 'primary' ? ( +
+ + {badgePrimary} +
+ ) : email.status === 'verified' ? ( +
+ + {badgeVerified} +
+ ) : ( +
+ + {badgePending} +
)} +
+ {email.status === 'pending' && ( -
- )} -
- ))} - {addingEmail ? ( -
- setNewEmail(e.target.value)} - /> - - -
- ) : ( + )} + +
+ )} +
+ ))} + {addingEmail ? ( +
+ setNewEmail(e.target.value)} + /> + - )} -
-
+
+ ) : ( + + )} + ); }; diff --git a/packages/react/src/screens/user-details-blocks/NameEdit.tsx b/packages/react/src/screens/user-details-blocks/NameEdit.tsx index 9ded00470..b03526985 100644 --- a/packages/react/src/screens/user-details-blocks/NameEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/NameEdit.tsx @@ -8,6 +8,7 @@ import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; import { CopyIcon } from '../../components/ui/icons/CopyIcon'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; +import UserDetailsCard from '../../components/user-details/UserDetailsCard'; const NameEdit: FC = () => { const { updateName } = useCorbado(); @@ -49,67 +50,64 @@ const NameEdit: FC = () => { } return ( -
- {headerName} -
- {!processUser.name && !editingName ? ( - + ) : ( +
+
+ setName(e.target.value)} + /> + void copyName()} /> - {buttonAddName} - - ) : ( -
-
- setName(e.target.value)} - /> - void copyName()} - /> -
- {editingName ? ( -
- - -
- ) : ( +
+ {editingName ? ( +
- )} -
- )} -
-
+ +
+ ) : ( + + )} +
+ )} + ); }; diff --git a/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx b/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx index b619da311..b73abd7d9 100644 --- a/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx @@ -10,6 +10,7 @@ import { VerifiedIcon } from '../../components/ui/icons/VerifiedIcon'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { getErrorCode } from '../../util'; +import UserDetailsCard from '../../components/user-details/UserDetailsCard'; const PhonesEdit = () => { const { createIdentifier, verifyIdentifierStart, verifyIdentifierFinish, deleteIdentifier } = useCorbado(); @@ -95,110 +96,110 @@ const PhonesEdit = () => { } return ( -
- {headerPhone} -
- {phones.map((phone, index) => ( -
- {verifyingPhones[index] ? ( -
- Enter OTP code for: {phone.value} - - setPhoneChallengeCodes(phoneChallengeCodes.map((c, i) => (i === index ? e.target.value : c))) - } - /> - - -
- ) : ( -
- {phone.value} -
- {phone.status === 'primary' ? ( -
- - {badgePrimary} -
- ) : phone.status === 'verified' ? ( -
- - {badgeVerified} -
- ) : ( -
- - {badgePending} -
- )} -
- {phone.status === 'pending' && ( - + + {phones.map((phone, index) => ( +
+ {verifyingPhones[index] ? ( +
+ Enter OTP code for: {phone.value} + + setPhoneChallengeCodes(phoneChallengeCodes.map((c, i) => (i === index ? e.target.value : c))) + } + /> + + +
+ ) : ( +
+ {phone.value} +
+ {phone.status === 'primary' ? ( +
+ + {badgePrimary} +
+ ) : phone.status === 'verified' ? ( +
+ + {badgeVerified} +
+ ) : ( +
+ + {badgePending} +
)} +
+ {phone.status === 'pending' && ( -
- )} -
- ))} - {addingPhone ? ( -
- setNewPhone(e.target.value)} - /> - - -
- ) : ( + )} + +
+ )} +
+ ))} + {addingPhone ? ( +
+ setNewPhone(e.target.value)} + /> + - )} -
-
+
+ ) : ( + + )} + ); }; diff --git a/packages/react/src/screens/user-details-blocks/UserDelete.tsx b/packages/react/src/screens/user-details-blocks/UserDelete.tsx index d7ab07fc6..a1a3978dd 100644 --- a/packages/react/src/screens/user-details-blocks/UserDelete.tsx +++ b/packages/react/src/screens/user-details-blocks/UserDelete.tsx @@ -1,15 +1,22 @@ import { t } from 'i18next'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { Button, Text } from '../../components'; import { useCorbado } from '../../hooks/useCorbado'; +import UserDetailsCard from '../../components/user-details/UserDetailsCard'; +import Alert from '../../components/user-details/Alert'; const UserDelete = () => { - const { deleteUser } = useCorbado(); + const { deleteUser, logout } = useCorbado(); + + const [isDeleting, setIsDeleting] = useState(false); const headerDelete = useMemo(() => t('user-details.delete_account'), [t]); const buttonDelete = useMemo(() => t('user-details.delete'), [t]); const textDelete = useMemo(() => t('user-details.delete_account_text'), [t]); + const titleDelete = useMemo(() => t('user-details.delete_account_title'), [t]); + const cancelDelete = useMemo(() => t('user-details.cancel'), [t]); + const confirmDelete = useMemo(() => t('user-details.confirm'), [t]); const deleteAccount = async () => { const res = await deleteUser(); @@ -18,24 +25,43 @@ const UserDelete = () => { console.error(res.val.message); return; } - // TODO: go back to login page? + + void logout(); }; return ( -
- {headerDelete} -
-
+ + {titleDelete} {textDelete} -
-
- -
+ {!isDeleting ? ( + + ) : ( + <> + + +
+ + + +
+ + )} + ); }; diff --git a/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx b/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx index 649f89c0b..26cdfc37c 100644 --- a/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx @@ -9,6 +9,7 @@ import { CopyIcon } from '../../components/ui/icons/CopyIcon'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { getErrorCode } from '../../util'; +import UserDetailsCard from '../../components/user-details/UserDetailsCard'; const UsernameEdit = () => { const { createIdentifier, updateUsername } = useCorbado(); @@ -65,106 +66,103 @@ const UsernameEdit = () => { } return ( -
- {headerUsername} -
- {!processUser.username ? ( -
- {addingUsername ? ( -
-
- setUsername({ id: '', type: 'username', status: 'verified', value: e.target.value })} - /> - void copyUsername()} - /> -
- - + + {!processUser.username ? ( +
+ {addingUsername ? ( +
+
+ setUsername({ id: '', type: 'username', status: 'verified', value: e.target.value })} + /> + void copyUsername()} + />
- ) : ( + +
+ ) : ( + + )} +
+ ) : ( +
+ {username && ( +
+
+ setUsername({ ...username, value: e.target.value })} + /> + void copyUsername()} /> - {buttonAddUsername} - - )} -
- ) : ( -
- {username && ( -
-
- setUsername({ ...username, value: e.target.value })} - /> - void copyUsername()} - /> -
- {editingUsername ? ( -
- - -
- ) : ( +
+ {editingUsername ? ( +
- )} -
- )} -
- )} -
-
+ +
+ ) : ( + + )} +
+ )} +
+ )} + ); }; diff --git a/packages/shared-ui/src/assets/alert.svg b/packages/shared-ui/src/assets/alert.svg new file mode 100644 index 000000000..b0b135c47 --- /dev/null +++ b/packages/shared-ui/src/assets/alert.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/packages/shared-ui/src/i18n/en.json b/packages/shared-ui/src/i18n/en.json index d16b8aec4..6a000cebf 100644 --- a/packages/shared-ui/src/i18n/en.json +++ b/packages/shared-ui/src/i18n/en.json @@ -415,7 +415,9 @@ "phone": "Phone number", "social": "Social accounts", "delete_account": "Delete account", + "delete_account_title": "Do you want to delete this account? ", "delete_account_text": "This will permanently remove your personal account and all of its content.", + "delete_account_alert": "This action is not reversible", "primary": "Primary", "verified": "Verified", "pending": "Pending", @@ -424,6 +426,7 @@ "save": "Save", "cancel": "Cancel", "change": "Change", + "confirm": "Confirm", "add_name": "Add name", "add_username": "Add username", "add_email": "Add email", diff --git a/packages/shared-ui/src/index.ts b/packages/shared-ui/src/index.ts index c8d242ca1..5b303c6de 100644 --- a/packages/shared-ui/src/index.ts +++ b/packages/shared-ui/src/index.ts @@ -1,6 +1,7 @@ import './styles/index.css'; import addIcon from './assets/add.svg'; +import alertIcon from './assets/alert.svg'; import appleIcon from './assets/apple.svg'; import cancelIcon from './assets/cancel.svg'; import changeIcon from './assets/change.svg'; @@ -59,6 +60,7 @@ export * from './flowHandler'; export type { BehaviorSubject } from 'rxjs'; export const assets = { + alertIcon, rightIcon, deleteIcon, passkeyDefaultIcon, diff --git a/packages/shared-ui/src/styles/typography.css b/packages/shared-ui/src/styles/typography.css index 131bed04b..4f7b44271 100644 --- a/packages/shared-ui/src/styles/typography.css +++ b/packages/shared-ui/src/styles/typography.css @@ -6,6 +6,10 @@ color: var(--cb-text-secondary-color); } +.cb-error-text-color { + color: var(--cb-error-text-color); +} + .cb-header-text-color { color: var(--cb-header-text-color); } diff --git a/packages/shared-ui/src/styles/user-details.css b/packages/shared-ui/src/styles/user-details.css index 43df472e2..85ed6be6a 100644 --- a/packages/shared-ui/src/styles/user-details.css +++ b/packages/shared-ui/src/styles/user-details.css @@ -27,25 +27,26 @@ } .cb-user-details-card { - /* display: flex; - flex-direction: row; */ + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: start; gap: 1rem; border-radius: var(--cb-border-radius-sm); border: var(--cb-passkey-list-border-color) 1px solid; margin: 0.5rem 0; padding: 1rem 1rem; background-color: var(--cb-white); + flex-wrap: wrap; line-height: 1.4; - overflow: hidden; } .cb-user-details-header { - margin: auto 0; margin-bottom: 0.5rem; font-size: calc(var(--cb-base-font-size) * 1.9); font-family: var(--cb-primary-font); + width: 170px; font-weight: bold; - float: left; } .cb-user-details-subheader { @@ -55,6 +56,7 @@ } .cb-user-details-text { + display: inline-block; font-size: calc(var(--cb-base-font-size) * 1.3); font-family: var(--cb-primary-font); } @@ -62,8 +64,6 @@ .cb-user-details-body { width: 100%; max-width: 500px; - float: right; - clear: right; } .cb-user-details-body-row { @@ -119,11 +119,22 @@ font-family: var(--cb-primary-font); } +.cb-user-details-body-button-delete { + color: var(--cb-error-text-color); + padding: 0.4rem 0; + background-color: var(--cb-color-primary); + border: 1px solid transparent; + border-radius: var(--cb-border-radius); + cursor: pointer; + box-sizing: border-box; + font-family: var(--cb-primary-font); +} + .cb-user-details-identifier-container { gap: 1rem; border-radius: var(--cb-border-radius-sm); border: var(--cb-border-color) 1px solid; - margin: 0.75rem 0; + margin-bottom: 0.75rem; padding: 0.6rem 0.5rem 0.6rem 1rem; } @@ -208,8 +219,25 @@ color: var(--cb-button-text-primary-color); } +.cb-user-details-alert-container { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; + margin: 0.5rem 0; +} + +.cb-user-details-alert-icon { + width: 13px; + height: 13px; +} + @media screen and (max-width: 481px) { .cb-user-details-section-indentifier { width: 75%; } + + .cb-user-details-body-subtitle { + display: none; + } } From 96f3071e41b95803751df5c8522d605be21599cf Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Fri, 16 Aug 2024 16:07:44 +0200 Subject: [PATCH 25/55] finishes name edit block --- .../screens/user-details-blocks/NameEdit.tsx | 18 ++++++--- .../user-details-blocks/UserDelete.tsx | 2 +- packages/shared-ui/src/i18n/en.json | 3 +- .../shared-ui/src/styles/user-details.css | 40 ++++++++++++++----- 4 files changed, 46 insertions(+), 17 deletions(-) diff --git a/packages/react/src/screens/user-details-blocks/NameEdit.tsx b/packages/react/src/screens/user-details-blocks/NameEdit.tsx index b03526985..10e828051 100644 --- a/packages/react/src/screens/user-details-blocks/NameEdit.tsx +++ b/packages/react/src/screens/user-details-blocks/NameEdit.tsx @@ -24,6 +24,8 @@ const NameEdit: FC = () => { const buttonCancel = useMemo(() => t('user-details.cancel'), [t]); const buttonChange = useMemo(() => t('user-details.change'), [t]); + const [errorMessage, setErrorMessage] = useState(undefined); + const copyName = async () => { if (name) { await navigator.clipboard.writeText(name); @@ -32,7 +34,7 @@ const NameEdit: FC = () => { const changeName = async () => { if (!name) { - console.error('name is empty'); + setErrorMessage(t('user-details.name_required')); return; } const res = await updateName(name); @@ -49,6 +51,12 @@ const NameEdit: FC = () => { return; } + const onCancel = () => { + setName(processUser.name); + setEditingName(false); + setErrorMessage(undefined); + }; + return ( {!processUser.name && !editingName ? ( @@ -66,11 +74,12 @@ const NameEdit: FC = () => {
setName(e.target.value)} + errorMessage={errorMessage} /> { diff --git a/packages/react/src/screens/user-details-blocks/UserDelete.tsx b/packages/react/src/screens/user-details-blocks/UserDelete.tsx index a1a3978dd..43c365ce3 100644 --- a/packages/react/src/screens/user-details-blocks/UserDelete.tsx +++ b/packages/react/src/screens/user-details-blocks/UserDelete.tsx @@ -53,7 +53,7 @@ const UserDelete = () => {
)}
- {email.status === 'pending' && ( - - )} - + { + if (item === buttonVerify) { + void startEmailVerification(index); + } else if (item === buttonRemove) { + void removeEmail(index); + } + }} + getItemClass={item => (item === buttonRemove ? 'cb-user-details-text-danger' : undefined)} + />
)}
diff --git a/packages/react/src/screens/user-details-blocks/NameEdit.tsx b/packages/react/src/screens/user-details/NameEdit.tsx similarity index 100% rename from packages/react/src/screens/user-details-blocks/NameEdit.tsx rename to packages/react/src/screens/user-details/NameEdit.tsx diff --git a/packages/react/src/screens/user-details-blocks/PhonesEdit.tsx b/packages/react/src/screens/user-details/PhonesEdit.tsx similarity index 100% rename from packages/react/src/screens/user-details-blocks/PhonesEdit.tsx rename to packages/react/src/screens/user-details/PhonesEdit.tsx diff --git a/packages/react/src/screens/user-details-blocks/UserDelete.tsx b/packages/react/src/screens/user-details/UserDelete.tsx similarity index 100% rename from packages/react/src/screens/user-details-blocks/UserDelete.tsx rename to packages/react/src/screens/user-details/UserDelete.tsx diff --git a/packages/react/src/screens/user-details-blocks/UsernameEdit.tsx b/packages/react/src/screens/user-details/UsernameEdit.tsx similarity index 100% rename from packages/react/src/screens/user-details-blocks/UsernameEdit.tsx rename to packages/react/src/screens/user-details/UsernameEdit.tsx diff --git a/packages/shared-ui/src/styles/user-details.css b/packages/shared-ui/src/styles/user-details.css index 540b1e71b..64178e154 100644 --- a/packages/shared-ui/src/styles/user-details.css +++ b/packages/shared-ui/src/styles/user-details.css @@ -67,8 +67,10 @@ } .cb-user-details-body-row { + width: 100%; display: flex; flex-direction: row; + justify-content: space-between; align-items: center; } @@ -254,6 +256,37 @@ } } +.cb-dropdown-menu { + position: relative; +} + +.cb-dropdown-menu-container { + position: absolute; + top: 30px; + right: 10px; + width: 120px; + background-color: var(--cb-white); + box-shadow: 0px 2px 6px 2px var(--cb-border-color); + border-radius: var(--cb-border-radius-sm); +} + +.cb-dropdown-menu-item { + padding: 0.3rem 0.5rem; + border-bottom: 1px solid var(--cb-border-color); +} + +.cb-dropdown-menu-item:last-child { + border-bottom: none; +} + +.cb-dropdown-menu-trigger { + cursor: pointer; + font-family: var(--cb-primary-font); + font-size: 14px; + font-weight: var(--cb-font-weight-bold); + margin-right: 4px; +} + @media screen and (max-width: 481px) { .cb-user-details-section-indentifier { width: 75%; From 171edb43795048b969f53070b6d00b5fea1ebb9d Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Fri, 16 Aug 2024 17:01:10 +0200 Subject: [PATCH 27/55] fixes lint and format issues --- packages/react/src/components/user-details/Alert.tsx | 4 +++- packages/react/src/components/user-details/DropdownMenu.tsx | 4 +++- .../react/src/components/user-details/UserDetailsCard.tsx | 3 ++- packages/react/src/screens/user-details/EmailsEdit.tsx | 4 ++-- packages/react/src/screens/user-details/NameEdit.tsx | 2 +- packages/react/src/screens/user-details/PhonesEdit.tsx | 2 +- packages/react/src/screens/user-details/UserDelete.tsx | 4 ++-- packages/react/src/screens/user-details/UsernameEdit.tsx | 2 +- 8 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/react/src/components/user-details/Alert.tsx b/packages/react/src/components/user-details/Alert.tsx index 9adf24a94..39e58bf60 100644 --- a/packages/react/src/components/user-details/Alert.tsx +++ b/packages/react/src/components/user-details/Alert.tsx @@ -1,4 +1,6 @@ -import React, { FC } from 'react'; +import type { FC } from 'react'; +import React from 'react'; + import { Text } from '../ui'; import { AlertIcon } from '../ui/icons/AlertIcon'; diff --git a/packages/react/src/components/user-details/DropdownMenu.tsx b/packages/react/src/components/user-details/DropdownMenu.tsx index 0b8f2a7d9..29966e8f9 100644 --- a/packages/react/src/components/user-details/DropdownMenu.tsx +++ b/packages/react/src/components/user-details/DropdownMenu.tsx @@ -1,4 +1,6 @@ -import React, { FC, useEffect, useRef } from 'react'; +import type { FC } from 'react'; +import React, { useEffect, useRef } from 'react'; + import { Text } from '../ui'; interface Props { diff --git a/packages/react/src/components/user-details/UserDetailsCard.tsx b/packages/react/src/components/user-details/UserDetailsCard.tsx index c7cea20c6..2043b9220 100644 --- a/packages/react/src/components/user-details/UserDetailsCard.tsx +++ b/packages/react/src/components/user-details/UserDetailsCard.tsx @@ -1,7 +1,8 @@ import type { FC, PropsWithChildren } from 'react'; -import { Text } from '../ui'; import React from 'react'; +import { Text } from '../ui'; + interface Props extends PropsWithChildren { header: string; } diff --git a/packages/react/src/screens/user-details/EmailsEdit.tsx b/packages/react/src/screens/user-details/EmailsEdit.tsx index 26026a320..60880f786 100644 --- a/packages/react/src/screens/user-details/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details/EmailsEdit.tsx @@ -7,11 +7,11 @@ import { AddIcon } from '../../components/ui/icons/AddIcon'; import { PendingIcon } from '../../components/ui/icons/PendingIcon'; import { PrimaryIcon } from '../../components/ui/icons/PrimaryIcon'; import { VerifiedIcon } from '../../components/ui/icons/VerifiedIcon'; +import DropdownMenu from '../../components/user-details/DropdownMenu'; +import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { getErrorCode } from '../../util'; -import UserDetailsCard from '../../components/user-details/UserDetailsCard'; -import DropdownMenu from '../../components/user-details/DropdownMenu'; const EmailsEdit = () => { const { createIdentifier, verifyIdentifierStart, verifyIdentifierFinish, deleteIdentifier } = useCorbado(); diff --git a/packages/react/src/screens/user-details/NameEdit.tsx b/packages/react/src/screens/user-details/NameEdit.tsx index 10e828051..f77cb7edd 100644 --- a/packages/react/src/screens/user-details/NameEdit.tsx +++ b/packages/react/src/screens/user-details/NameEdit.tsx @@ -6,9 +6,9 @@ import { Button, InputField, Text } from '../../components'; import { AddIcon } from '../../components/ui/icons/AddIcon'; import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; import { CopyIcon } from '../../components/ui/icons/CopyIcon'; +import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; -import UserDetailsCard from '../../components/user-details/UserDetailsCard'; const NameEdit: FC = () => { const { updateName } = useCorbado(); diff --git a/packages/react/src/screens/user-details/PhonesEdit.tsx b/packages/react/src/screens/user-details/PhonesEdit.tsx index b73abd7d9..287277bf7 100644 --- a/packages/react/src/screens/user-details/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details/PhonesEdit.tsx @@ -7,10 +7,10 @@ import { AddIcon } from '../../components/ui/icons/AddIcon'; import { PendingIcon } from '../../components/ui/icons/PendingIcon'; import { PrimaryIcon } from '../../components/ui/icons/PrimaryIcon'; import { VerifiedIcon } from '../../components/ui/icons/VerifiedIcon'; +import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { getErrorCode } from '../../util'; -import UserDetailsCard from '../../components/user-details/UserDetailsCard'; const PhonesEdit = () => { const { createIdentifier, verifyIdentifierStart, verifyIdentifierFinish, deleteIdentifier } = useCorbado(); diff --git a/packages/react/src/screens/user-details/UserDelete.tsx b/packages/react/src/screens/user-details/UserDelete.tsx index 43c365ce3..df4cacf22 100644 --- a/packages/react/src/screens/user-details/UserDelete.tsx +++ b/packages/react/src/screens/user-details/UserDelete.tsx @@ -2,9 +2,9 @@ import { t } from 'i18next'; import React, { useMemo, useState } from 'react'; import { Button, Text } from '../../components'; -import { useCorbado } from '../../hooks/useCorbado'; -import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import Alert from '../../components/user-details/Alert'; +import UserDetailsCard from '../../components/user-details/UserDetailsCard'; +import { useCorbado } from '../../hooks/useCorbado'; const UserDelete = () => { const { deleteUser, logout } = useCorbado(); diff --git a/packages/react/src/screens/user-details/UsernameEdit.tsx b/packages/react/src/screens/user-details/UsernameEdit.tsx index 26cdfc37c..513138fd8 100644 --- a/packages/react/src/screens/user-details/UsernameEdit.tsx +++ b/packages/react/src/screens/user-details/UsernameEdit.tsx @@ -6,10 +6,10 @@ import { Button, InputField, Text } from '../../components'; import { AddIcon } from '../../components/ui/icons/AddIcon'; import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; import { CopyIcon } from '../../components/ui/icons/CopyIcon'; +import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { getErrorCode } from '../../util'; -import UserDetailsCard from '../../components/user-details/UserDetailsCard'; const UsernameEdit = () => { const { createIdentifier, updateUsername } = useCorbado(); From c8568d3a9ead213ce969206326637f4e768eb16b Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Mon, 19 Aug 2024 10:11:40 +0200 Subject: [PATCH 28/55] PR --- .../src/contexts/CorbadoSessionContext.tsx | 12 +++++----- .../src/contexts/CorbadoSessionProvider.tsx | 22 ++++++++--------- .../src/screens/user-details/NameEdit.tsx | 4 ++-- .../web-core/src/services/SessionService.ts | 18 +++++++------- packages/web-js/src/core/Corbado.ts | 24 +++++++++---------- playground/web-js-script/index.html | 2 +- playground/web-js/src/scripts/index.js | 2 +- 7 files changed, 42 insertions(+), 42 deletions(-) diff --git a/packages/react/src/contexts/CorbadoSessionContext.tsx b/packages/react/src/contexts/CorbadoSessionContext.tsx index 25e3e144b..55db069af 100644 --- a/packages/react/src/contexts/CorbadoSessionContext.tsx +++ b/packages/react/src/contexts/CorbadoSessionContext.tsx @@ -19,12 +19,12 @@ export interface CorbadoSessionContextProps { deletePasskey: (id: string) => Promise>; getFullUser: (abortController?: AbortController) => Promise>; getIdentifierListConfig: (abortController?: AbortController) => Promise>; - updateName: (fullName: string) => Promise>; - updateUsername: (identifierID: string, username: string) => Promise>; + updateFullName: (fullName: string) => Promise>; + updateUsername: (identifierId: string, username: string) => Promise>; createIdentifier: (identifierType: LoginIdentifierType, value: string) => Promise>; - deleteIdentifier: (identifierID: string) => Promise>; - verifyIdentifierStart: (identifierID: string) => Promise>; - verifyIdentifierFinish: (identifierID: string, code: string) => Promise>; + deleteIdentifier: (identifierId: string) => Promise>; + verifyIdentifierStart: (identifierId: string) => Promise>; + verifyIdentifierFinish: (identifierId: string, code: string) => Promise>; deleteUser: () => Promise>; globalError: NonRecoverableError | undefined; } @@ -42,7 +42,7 @@ export const initialContext: CorbadoSessionContextProps = { deletePasskey: missingImplementation, getFullUser: missingImplementation, getIdentifierListConfig: missingImplementation, - updateName: missingImplementation, + updateFullName: missingImplementation, updateUsername: missingImplementation, createIdentifier: missingImplementation, deleteIdentifier: missingImplementation, diff --git a/packages/react/src/contexts/CorbadoSessionProvider.tsx b/packages/react/src/contexts/CorbadoSessionProvider.tsx index ccb0f54c8..4621e1d82 100644 --- a/packages/react/src/contexts/CorbadoSessionProvider.tsx +++ b/packages/react/src/contexts/CorbadoSessionProvider.tsx @@ -93,16 +93,16 @@ export const CorbadoSessionProvider: FC = ({ [corbadoApp], ); - const updateName = useCallback( + const updateFullName = useCallback( (fullName: string) => { - return corbadoApp.sessionService.updateName(fullName); + return corbadoApp.sessionService.updateFullName(fullName); }, [corbadoApp], ); const updateUsername = useCallback( - (identifierID: string, username: string) => { - return corbadoApp.sessionService.updateUsername(identifierID, username); + (identifierId: string, username: string) => { + return corbadoApp.sessionService.updateUsername(identifierId, username); }, [corbadoApp], ); @@ -115,22 +115,22 @@ export const CorbadoSessionProvider: FC = ({ ); const deleteIdentifier = useCallback( - (identifierID: string) => { - return corbadoApp.sessionService.deleteIdentifier(identifierID); + (identifierId: string) => { + return corbadoApp.sessionService.deleteIdentifier(identifierId); }, [corbadoApp], ); const verifyIdentifierStart = useCallback( - (identifierID: string) => { - return corbadoApp.sessionService.verifyIdentifierStart(identifierID); + (identifierId: string) => { + return corbadoApp.sessionService.verifyIdentifierStart(identifierId); }, [corbadoApp], ); const verifyIdentifierFinish = useCallback( - (identifierID: string, code: string) => { - return corbadoApp.sessionService.verifyIdentifierFinish(identifierID, code); + (identifierId: string, code: string) => { + return corbadoApp.sessionService.verifyIdentifierFinish(identifierId, code); }, [corbadoApp], ); @@ -150,7 +150,7 @@ export const CorbadoSessionProvider: FC = ({ appendPasskey, getFullUser, getIdentifierListConfig, - updateName, + updateFullName, updateUsername, createIdentifier, deleteIdentifier, diff --git a/packages/react/src/screens/user-details/NameEdit.tsx b/packages/react/src/screens/user-details/NameEdit.tsx index f77cb7edd..2cdd95fee 100644 --- a/packages/react/src/screens/user-details/NameEdit.tsx +++ b/packages/react/src/screens/user-details/NameEdit.tsx @@ -11,7 +11,7 @@ import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; const NameEdit: FC = () => { - const { updateName } = useCorbado(); + const { updateFullName } = useCorbado(); const { name, getCurrentUser, processUser, setName, fullNameRequired } = useCorbadoUserDetails(); const { t } = useTranslation('translation'); @@ -37,7 +37,7 @@ const NameEdit: FC = () => { setErrorMessage(t('user-details.name_required')); return; } - const res = await updateName(name); + const res = await updateFullName(name); if (res.err) { // no possible error code console.error(res.val.message); diff --git a/packages/web-core/src/services/SessionService.ts b/packages/web-core/src/services/SessionService.ts index a322f8dcb..f321d1bc5 100644 --- a/packages/web-core/src/services/SessionService.ts +++ b/packages/web-core/src/services/SessionService.ts @@ -171,17 +171,17 @@ export class SessionService { return this.wrapWithErr(async () => this.#configsApi.getIdentifierListConfig({ signal: abortController.signal })); } - async updateName(fullName: string): Promise> { + async updateFullName(fullName: string): Promise> { return Result.wrapAsync(async () => { await this.#usersApi.currentUserUpdate({ fullName }); return void 0; }); } - async updateUsername(identifierID: string, username: string): Promise> { + async updateUsername(identifierId: string, username: string): Promise> { return Result.wrapAsync(async () => { await this.#usersApi.currentUserIdentifierUpdate({ - identifierID, + identifierID: identifierId, identifierType: LoginIdentifierType.Username, value: username, }); @@ -196,17 +196,17 @@ export class SessionService { }); } - async deleteIdentifier(identifierID: string): Promise> { + async deleteIdentifier(identifierId: string): Promise> { return Result.wrapAsync(async () => { - await this.#usersApi.currentUserIdentifierDelete({ identifierID }); + await this.#usersApi.currentUserIdentifierDelete({ identifierID: identifierId }); return void 0; }); } - async verifyIdentifierStart(identifierID: string): Promise> { + async verifyIdentifierStart(identifierId: string): Promise> { return Result.wrapAsync(async () => { await this.#usersApi.currentUserIdentifierVerifyStart({ - identifierID, + identifierID: identifierId, clientInformation: { bluetoothAvailable: (await WebAuthnService.canUseBluetooth()) ?? false, canUsePasskeys: await WebAuthnService.doesBrowserSupportPasskeys(), @@ -218,9 +218,9 @@ export class SessionService { }); } - async verifyIdentifierFinish(identifierID: string, code: string): Promise> { + async verifyIdentifierFinish(identifierId: string, code: string): Promise> { return Result.wrapAsync(async () => { - await this.#usersApi.currentUserIdentifierVerifyFinish({ identifierID, code }); + await this.#usersApi.currentUserIdentifierVerifyFinish({ identifierID: identifierId, code }); return void 0; }); } diff --git a/packages/web-js/src/core/Corbado.ts b/packages/web-js/src/core/Corbado.ts index ef3c9c53b..9e78bea27 100644 --- a/packages/web-js/src/core/Corbado.ts +++ b/packages/web-js/src/core/Corbado.ts @@ -74,11 +74,11 @@ export class Corbado { this.#unmountComponent(element); } - mountUserUI(element: HTMLElement) { + mountUserDetailsUI(element: HTMLElement) { this.#mountComponent(element, UserDetails, {}); } - unmountUserUI(element: HTMLElement) { + unmountUserDetailsUI(element: HTMLElement) { this.#unmountComponent(element); } @@ -112,28 +112,28 @@ export class Corbado { ); } - updateName(fullName: string) { - return this.#getCorbadoAppState().corbadoApp.sessionService.updateName(fullName); + updateFullName(fullName: string) { + return this.#getCorbadoAppState().corbadoApp.sessionService.updateFullName(fullName); } - updateUsername(identifierID: string, username: string) { - return this.#getCorbadoAppState().corbadoApp.sessionService.updateUsername(identifierID, username); + updateUsername(identifierId: string, username: string) { + return this.#getCorbadoAppState().corbadoApp.sessionService.updateUsername(identifierId, username); } createIdentifier(identifierType: LoginIdentifierType, value: string) { return this.#getCorbadoAppState().corbadoApp.sessionService.createIdentifier(identifierType, value); } - deleteIdentifier(identifierID: string) { - return this.#getCorbadoAppState().corbadoApp.sessionService.deleteIdentifier(identifierID); + deleteIdentifier(identifierId: string) { + return this.#getCorbadoAppState().corbadoApp.sessionService.deleteIdentifier(identifierId); } - verifyIdentifierStart(identifierID: string) { - return this.#getCorbadoAppState().corbadoApp.sessionService.verifyIdentifierStart(identifierID); + verifyIdentifierStart(identifierId: string) { + return this.#getCorbadoAppState().corbadoApp.sessionService.verifyIdentifierStart(identifierId); } - verifyIdentifierFinish(identifierID: string, code: string) { - return this.#getCorbadoAppState().corbadoApp.sessionService.verifyIdentifierFinish(identifierID, code); + verifyIdentifierFinish(identifierId: string, code: string) { + return this.#getCorbadoAppState().corbadoApp.sessionService.verifyIdentifierFinish(identifierId, code); } deleteUser() { diff --git a/playground/web-js-script/index.html b/playground/web-js-script/index.html index 3c746b52d..b50f49744 100644 --- a/playground/web-js-script/index.html +++ b/playground/web-js-script/index.html @@ -58,7 +58,7 @@

Passkey Lists

const shortSession = document.getElementById('short-session'); shortSession.style.maxWidth = '550px'; - Corbado.mountUserUI(shortSession); + Corbado.mountUserDetailsUI(shortSession); const logoutButton = document.getElementById('logout'); logoutButton.addEventListener('click', async () => { diff --git a/playground/web-js/src/scripts/index.js b/playground/web-js/src/scripts/index.js index d3e1e9501..7104e4331 100644 --- a/playground/web-js/src/scripts/index.js +++ b/playground/web-js/src/scripts/index.js @@ -23,7 +23,7 @@ if (!Corbado.user) { const shortSession = document.getElementById('short-session'); shortSession.style.maxWidth = '550px'; - Corbado.mountUserUI(shortSession); + Corbado.mountUserDetailsUI(shortSession); const logoutButton = document.getElementById('logout'); logoutButton.addEventListener('click', async () => { From b3e5c522721ca2cde704a13f208f76388171245e Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Mon, 19 Aug 2024 11:59:15 +0200 Subject: [PATCH 29/55] adds identifier deletion process --- .../src/components/user-details/Alert.tsx | 7 +- .../components/user-details/DropdownMenu.tsx | 17 +-- .../src/screens/user-details/EmailsEdit.tsx | 85 ++++++++------- .../user-details/IdentifierDeleteDialog.tsx | 101 ++++++++++++++++++ .../src/screens/user-details/PhonesEdit.tsx | 95 ++++++++-------- .../src/screens/user-details/UserDelete.tsx | 2 +- packages/shared-ui/src/i18n/en.json | 12 ++- packages/shared-ui/src/styles/common.css | 5 + .../shared-ui/src/styles/user-details.css | 64 ++++++++++- packages/shared-ui/src/styles/variables.css | 3 + 10 files changed, 284 insertions(+), 107 deletions(-) create mode 100644 packages/react/src/screens/user-details/IdentifierDeleteDialog.tsx diff --git a/packages/react/src/components/user-details/Alert.tsx b/packages/react/src/components/user-details/Alert.tsx index 39e58bf60..73e4d5da6 100644 --- a/packages/react/src/components/user-details/Alert.tsx +++ b/packages/react/src/components/user-details/Alert.tsx @@ -6,16 +6,17 @@ import { AlertIcon } from '../ui/icons/AlertIcon'; interface Props { text: string; + variant?: 'error' | 'info'; } -const Alert: FC = ({ text }) => { +const Alert: FC = ({ text, variant = 'error' }) => { return (
- {text} + {text}
); }; diff --git a/packages/react/src/components/user-details/DropdownMenu.tsx b/packages/react/src/components/user-details/DropdownMenu.tsx index 29966e8f9..a06fd92e8 100644 --- a/packages/react/src/components/user-details/DropdownMenu.tsx +++ b/packages/react/src/components/user-details/DropdownMenu.tsx @@ -1,15 +1,13 @@ import type { FC } from 'react'; import React, { useEffect, useRef } from 'react'; - import { Text } from '../ui'; - interface Props { items: string[]; onItemClick: (item: string) => void; - getItemClass: (item: string) => string | undefined; + getItemClassName: (item: string) => string | undefined; } -const DropdownMenu: FC = ({ items, onItemClick, getItemClass }) => { +const DropdownMenu: FC = ({ items, onItemClick, getItemClassName }) => { const [visible, setVisible] = React.useState(false); const menuRef = useRef(null); @@ -36,17 +34,20 @@ const DropdownMenu: FC = ({ items, onItemClick, getItemClass }) => { className='cb-dropdown-menu-trigger' onClick={() => setVisible(!visible)} > - ... + ⋯
{visible && (
{items.map((item, index) => (
onItemClick(item)} + onClick={() => { + onItemClick(item); + setVisible(false); + }} + className='cb-dropdown-menu-item' > - {item} + {item}
))}
diff --git a/packages/react/src/screens/user-details/EmailsEdit.tsx b/packages/react/src/screens/user-details/EmailsEdit.tsx index 60880f786..0dcfd0a53 100644 --- a/packages/react/src/screens/user-details/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details/EmailsEdit.tsx @@ -12,14 +12,17 @@ import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { getErrorCode } from '../../util'; +import { Identifier } from '@corbado/types'; +import IdentifierDeleteDialog from './IdentifierDeleteDialog'; const EmailsEdit = () => { - const { createIdentifier, verifyIdentifierStart, verifyIdentifierFinish, deleteIdentifier } = useCorbado(); + const { createIdentifier, verifyIdentifierStart, verifyIdentifierFinish } = useCorbado(); const { emails = [], getCurrentUser, emailEnabled } = useCorbadoUserDetails(); const [verifyingEmails, setVerifyingEmails] = useState([]); const [emailChallengeCodes, setEmailChallengeCodes] = useState([]); const [addingEmail, setAddingEmail] = useState(false); + const [deletingEmail, setDeletingEmail] = useState(false); const [newEmail, setNewEmail] = useState(''); const headerEmail = useMemo(() => t('user-details.email'), [t]); @@ -53,19 +56,6 @@ const EmailsEdit = () => { void getCurrentUser(); }; - const removeEmail = async (index: number) => { - const res = await deleteIdentifier(emails[index].id); - if (res.err) { - const code = getErrorCode(res.val.message); - if (code) { - // possible codes: no_remaining_identifier, no_remaining_verified_identifier - console.error(t(`errors.${code}`)); - } - return; - } - void getCurrentUser(); - }; - const startEmailVerification = async (index: number) => { const res = await verifyIdentifierStart(emails[index].id); if (res.err) { @@ -96,6 +86,19 @@ const EmailsEdit = () => { return null; } + const getBadge = (email: Identifier) => { + switch (email.status) { + case 'primary': + return { text: badgePrimary, icon: }; + + case 'verified': + return { text: badgeVerified, icon: }; + + default: + return { text: badgePending, icon: }; + } + }; + return ( {emails.map((email, index) => ( @@ -126,38 +129,34 @@ const EmailsEdit = () => {
) : ( -
- {email.value} -
- {email.status === 'primary' ? ( -
- - {badgePrimary} -
- ) : email.status === 'verified' ? ( -
- - {badgeVerified} -
- ) : ( + <> +
+
+ {email.value}
- - {badgePending} + {getBadge(email).icon} + {getBadge(email).text}
- )} +
+ { + if (item === buttonVerify) { + void startEmailVerification(index); + } else if (item === buttonRemove) { + setDeletingEmail(true); + } + }} + getItemClassName={item => (item === buttonRemove ? 'cb-error-text-color' : '')} + />
- { - if (item === buttonVerify) { - void startEmailVerification(index); - } else if (item === buttonRemove) { - void removeEmail(index); - } - }} - getItemClass={item => (item === buttonRemove ? 'cb-user-details-text-danger' : undefined)} - /> -
+ {deletingEmail && ( + setDeletingEmail(false)} + /> + )} + )}
))} diff --git a/packages/react/src/screens/user-details/IdentifierDeleteDialog.tsx b/packages/react/src/screens/user-details/IdentifierDeleteDialog.tsx new file mode 100644 index 000000000..e21d3a0aa --- /dev/null +++ b/packages/react/src/screens/user-details/IdentifierDeleteDialog.tsx @@ -0,0 +1,101 @@ +import { Identifier } from '@corbado/types'; +import React, { FC } from 'react'; +import { Button, Text } from '../../components'; +import Alert from '../../components/user-details/Alert'; +import { useCorbado } from '../../hooks/useCorbado'; +import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; +import { getErrorCode } from '../../util'; +import { useTranslation } from 'react-i18next'; + +interface Props { + identifier: Identifier; + onCancel: () => void; +} + +const IdentifierDeleteDialog: FC = ({ identifier, onCancel }) => { + const { t } = useTranslation('translation'); + const { deleteIdentifier } = useCorbado(); + const { getCurrentUser } = useCorbadoUserDetails(); + + const removeEmail = async () => { + const res = await deleteIdentifier(identifier.id); + if (res.err) { + const code = getErrorCode(res.val.message); + if (code) { + // possible codes: no_remaining_identifier, no_remaining_verified_identifier + console.error(t(`errors.${code}`)); + } + return; + } + void getCurrentUser(); + }; + + const getHeading = () => { + switch (identifier.type) { + case 'email': + return t('user-details.email_delete.header'); + case 'phone': + return t('user-details.phone_delete.header'); + default: + return ''; + } + }; + + const getBody = () => { + switch (identifier.type) { + case 'email': + return t('user-details.email_delete.body'); + case 'phone': + return t('user-details.phone_delete.body'); + default: + return ''; + } + }; + + const getAlert = () => { + switch (identifier.type) { + case 'email': + return t('user-details.email_delete.alert'); + case 'phone': + return t('user-details.phone_delete.alert'); + default: + return ''; + } + }; + + return ( +
+ + {getHeading()} + + {getBody()} + + +
+ + +
+
+ ); +}; + +export default IdentifierDeleteDialog; diff --git a/packages/react/src/screens/user-details/PhonesEdit.tsx b/packages/react/src/screens/user-details/PhonesEdit.tsx index 287277bf7..1821d4457 100644 --- a/packages/react/src/screens/user-details/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details/PhonesEdit.tsx @@ -11,13 +11,17 @@ import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { getErrorCode } from '../../util'; +import { Identifier } from '@corbado/types'; +import DropdownMenu from '../../components/user-details/DropdownMenu'; +import IdentifierDeleteDialog from './IdentifierDeleteDialog'; const PhonesEdit = () => { - const { createIdentifier, verifyIdentifierStart, verifyIdentifierFinish, deleteIdentifier } = useCorbado(); - const { phones = [], getCurrentUser, phoneEnabled } = useCorbadoUserDetails(); + const { createIdentifier, verifyIdentifierStart, verifyIdentifierFinish } = useCorbado(); + const { phones = [], getCurrentUser } = useCorbadoUserDetails(); const [verifyingPhones, setVerifyingPhones] = useState([]); const [phoneChallengeCodes, setPhoneChallengeCodes] = useState([]); + const [deletingPhone, setDeletingPhone] = useState(false); const [addingPhone, setAddingPhone] = useState(false); const [newPhone, setNewPhone] = useState(''); @@ -52,19 +56,6 @@ const PhonesEdit = () => { void getCurrentUser(); }; - const removePhone = async (index: number) => { - const res = await deleteIdentifier(phones[index].id); - if (res.err) { - const code = getErrorCode(res.val.message); - if (code) { - // possible codes: no_remaining_identifier, no_remaining_verified_identifier - console.error(t(`errors.${code}`)); - } - return; - } - void getCurrentUser(); - }; - const startPhoneVerification = async (index: number) => { const res = await verifyIdentifierStart(phones[index].id); if (res.err) { @@ -91,9 +82,22 @@ const PhonesEdit = () => { void getCurrentUser(); }; - if (!phoneEnabled) { - return null; - } + // if (!phoneEnabled) { + // return null; + // } + + const getBadge = (phone: Identifier) => { + switch (phone.status) { + case 'primary': + return { text: badgePrimary, icon: }; + + case 'verified': + return { text: badgeVerified, icon: }; + + default: + return { text: badgePending, icon: }; + } + }; return ( @@ -125,41 +129,34 @@ const PhonesEdit = () => {
) : ( -
- {phone.value} -
- {phone.status === 'primary' ? ( + <> +
+
+ {phone.value}
- - {badgePrimary} + {getBadge(phone).icon} + {getBadge(phone).text}
- ) : phone.status === 'verified' ? ( -
- - {badgeVerified} -
- ) : ( -
- - {badgePending} -
- )} +
+ { + if (item === buttonVerify) { + void startPhoneVerification(index); + } else if (item === buttonRemove) { + setDeletingPhone(true); + } + }} + getItemClassName={item => (item === buttonRemove ? 'cb-error-text-color' : '')} + />
- {phone.status === 'pending' && ( - + {deletingPhone && ( + setDeletingPhone(false)} + /> )} - -
+ )}
))} diff --git a/packages/react/src/screens/user-details/UserDelete.tsx b/packages/react/src/screens/user-details/UserDelete.tsx index df4cacf22..ee188412b 100644 --- a/packages/react/src/screens/user-details/UserDelete.tsx +++ b/packages/react/src/screens/user-details/UserDelete.tsx @@ -44,7 +44,7 @@ const UserDelete = () => { <> -
+
- -
+ onFinishEmailVerification(index)} + /> ) : ( <>
@@ -139,21 +127,23 @@ const EmailsEdit = () => {
{ if (item === buttonVerify) { void startEmailVerification(index); } else if (item === buttonRemove) { - setDeletingEmail(true); + setDeletingEmail(email); + } else { + copyEmail(email.value); } }} getItemClassName={item => (item === buttonRemove ? 'cb-error-text-color' : '')} />
- {deletingEmail && ( + {deletingEmail === email && ( setDeletingEmail(false)} + onCancel={() => setDeletingEmail(undefined)} /> )} @@ -163,11 +153,10 @@ const EmailsEdit = () => { {addingEmail ? (
setNewEmail(e.target.value)} + errorMessage={errorMessage} /> +
+ ); +}; + +export default IdentifierVerifyDialog; diff --git a/packages/react/src/screens/user-details/PhonesEdit.tsx b/packages/react/src/screens/user-details/PhonesEdit.tsx index 1821d4457..9f497958a 100644 --- a/packages/react/src/screens/user-details/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details/PhonesEdit.tsx @@ -1,6 +1,6 @@ import { LoginIdentifierType } from '@corbado/shared-ui'; import { t } from 'i18next'; -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Button, InputField, Text } from '../../components'; import { AddIcon } from '../../components/ui/icons/AddIcon'; @@ -14,14 +14,14 @@ import { getErrorCode } from '../../util'; import { Identifier } from '@corbado/types'; import DropdownMenu from '../../components/user-details/DropdownMenu'; import IdentifierDeleteDialog from './IdentifierDeleteDialog'; +import IdentifierVerifyDialog from './IdentifierVerifyDialog'; const PhonesEdit = () => { - const { createIdentifier, verifyIdentifierStart, verifyIdentifierFinish } = useCorbado(); - const { phones = [], getCurrentUser } = useCorbadoUserDetails(); + const { createIdentifier, verifyIdentifierStart } = useCorbado(); + const { phones = [], getCurrentUser, phoneEnabled } = useCorbadoUserDetails(); const [verifyingPhones, setVerifyingPhones] = useState([]); - const [phoneChallengeCodes, setPhoneChallengeCodes] = useState([]); - const [deletingPhone, setDeletingPhone] = useState(false); + const [deletingPhone, setDeletingPhone] = useState(); const [addingPhone, setAddingPhone] = useState(false); const [newPhone, setNewPhone] = useState(''); @@ -32,11 +32,16 @@ const PhonesEdit = () => { const badgePending = useMemo(() => t('user-details.pending'), [t]); const buttonSave = useMemo(() => t('user-details.save'), [t]); + const buttonCopy = useMemo(() => t('user-details.copy'), [t]); const buttonCancel = useMemo(() => t('user-details.cancel'), [t]); const buttonAddPhone = useMemo(() => t('user-details.add_phone'), [t]); const buttonVerify = useMemo(() => t('user-details.verify'), [t]); const buttonRemove = useMemo(() => t('user-details.remove'), [t]); + useEffect(() => { + setVerifyingPhones(new Array(phones.length).fill(false)); + }, [phones]); + const addPhone = async () => { if (!newPhone) { console.error('phone is empty'); @@ -58,6 +63,7 @@ const PhonesEdit = () => { const startPhoneVerification = async (index: number) => { const res = await verifyIdentifierStart(phones[index].id); + if (res.err) { const code = getErrorCode(res.val.message); if (code) { @@ -66,25 +72,17 @@ const PhonesEdit = () => { } return; } - setVerifyingPhones(verifyingPhones.map((v, i) => (i === index ? true : v))); + + setVerifyingPhones(prev => prev.map((v, i) => (i === index ? true : v))); }; - const finishPhoneVerification = async (index: number) => { - const res = await verifyIdentifierFinish(phones[index].id, phoneChallengeCodes[index]); - if (res.err) { - const code = getErrorCode(res.val.message); - if (code) { - // possible code: invalid_challenge_solution_phone-otp - console.error(t(`errors.${code}`)); - } - return; - } - void getCurrentUser(); + const onFinishEmailVerification = (index: number) => { + setVerifyingPhones(prev => prev.map((v, i) => (i === index ? false : v))); }; - // if (!phoneEnabled) { - // return null; - // } + if (!phoneEnabled) { + return null; + } const getBadge = (phone: Identifier) => { switch (phone.status) { @@ -107,27 +105,10 @@ const PhonesEdit = () => { key={index} > {verifyingPhones[index] ? ( -
- Enter OTP code for: {phone.value} - - setPhoneChallengeCodes(phoneChallengeCodes.map((c, i) => (i === index ? e.target.value : c))) - } - /> - - -
+ onFinishEmailVerification(index)} + /> ) : ( <>
@@ -139,12 +120,12 @@ const PhonesEdit = () => {
{ if (item === buttonVerify) { void startPhoneVerification(index); } else if (item === buttonRemove) { - setDeletingPhone(true); + setDeletingPhone(phone); } }} getItemClassName={item => (item === buttonRemove ? 'cb-error-text-color' : '')} @@ -153,7 +134,7 @@ const PhonesEdit = () => { {deletingPhone && ( setDeletingPhone(false)} + onCancel={() => setDeletingPhone(undefined)} /> )} diff --git a/packages/react/src/screens/user-details/UsernameEdit.tsx b/packages/react/src/screens/user-details/UsernameEdit.tsx index 513138fd8..a1b1fcc52 100644 --- a/packages/react/src/screens/user-details/UsernameEdit.tsx +++ b/packages/react/src/screens/user-details/UsernameEdit.tsx @@ -119,7 +119,7 @@ const UsernameEdit = () => {
{ const matches = regex.exec(message); return matches ? matches[1] : undefined; }; + +export function validateEmail(email: string): boolean { + const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + return emailRegex.test(email); +} diff --git a/packages/shared-ui/src/i18n/en.json b/packages/shared-ui/src/i18n/en.json index dd11c6a58..9fca1ccdc 100644 --- a/packages/shared-ui/src/i18n/en.json +++ b/packages/shared-ui/src/i18n/en.json @@ -433,6 +433,9 @@ "add_phone": "Add phone", "delete": "Delete", "warning_notLoggedIn": "Please log in to see your current user information.", + "warning_invalid_email": "Please enter a valid email", + "warning_invalid_phone": "Please enter a valid phone number", + "warning_invalid_challenge": "Please enter the correct code", "providers": { "google": "Google", "microsoft": "Microsoft", @@ -444,10 +447,20 @@ "body": "Are you sure you want to remove this email address ?", "alert": "You will no longer be able to log in with this email address. " }, + "email_verify": { + "header": "Verify your email address", + "body": "Please verify your email by entering the one-time passcode we just sent to your email.", + "link": "Didn’t receive an email? Resend ({{counter}}s)" + }, "phone_delete": { "header": "Remove Phone Number", "body": "Are you sure you want to remove this phone number ?", "alert": "You will no longer be able to log in with this phone number. " + }, + "phone_verify": { + "header": "Verify your phone number", + "body": "Please verify your phone number by entering the one-time passcode we just sent to your phone number.", + "link": "Didn’t receive an SMS? Resend ({{counter}}s)" } } } diff --git a/packages/shared-ui/src/styles/user-details.css b/packages/shared-ui/src/styles/user-details.css index 4a23bfe12..e0890a1d4 100644 --- a/packages/shared-ui/src/styles/user-details.css +++ b/packages/shared-ui/src/styles/user-details.css @@ -116,7 +116,8 @@ } .cb-user-details-body-button-secondary, -.cb-user-details-body-button-cancel { +.cb-user-details-body-button-cancel, +.cb-user-details-verify-identifier-button-cancel { padding: 0.4rem 3rem; background-color: var(--cb-secondary-color); border: 1px solid transparent; @@ -126,6 +127,11 @@ font-family: var(--cb-primary-font); } +.cb-user-details-verify-identifier-button-cancel { + align-self: flex-end; + width: fit-content; +} + .cb-user-details-body-button-secondary span { color: var(--cb-secondary-link-color); font-size: 10px; @@ -261,6 +267,7 @@ } .cb-dropdown-menu-container { + z-index: 1000; position: absolute; top: 30px; right: 10px; @@ -343,6 +350,17 @@ button.cb-user-details-deletion-dialog-secondary-button:hover { gap: 1rem; } +.cb-otp-inputs-container { + width: 260px; + margin-bottom: 1rem; +} + +.cb-user-details-verify-identifier-container { + display: flex; + flex-direction: column; + gap: 6px; +} + @media screen and (max-width: 481px) { .cb-user-details-section-indentifier { width: 75%; From e12afd43370e834bf00d2f67062379154c7f7f62 Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Mon, 19 Aug 2024 16:34:05 +0200 Subject: [PATCH 33/55] adds loading to OTP --- .../user-details/IdentifierVerifyDialog.tsx | 5 +++-- .../src/screens/user-details/PhonesEdit.tsx | 19 +++++++++-------- packages/shared-ui/src/i18n/en.json | 1 + .../shared-ui/src/styles/user-details.css | 21 ++++++++++++++++++- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx b/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx index 894f3e55c..867bc1003 100644 --- a/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx +++ b/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx @@ -1,5 +1,5 @@ import React, { FC, MouseEvent, useCallback, useEffect, useRef, useState } from 'react'; -import { Button, Link, OtpInputGroup, Text } from '../../components'; +import { Button, Link, LoadingSpinner, OtpInputGroup, Text } from '../../components'; import { Identifier } from '@corbado/types'; import { useTranslation } from 'react-i18next'; import { getErrorCode } from '../../util'; @@ -123,7 +123,7 @@ const IdentifierVerifyDialog: FC = ({ identifier, onCancel }) => { return; } - startTimer(); + setRemainingTime(30); }; return ( @@ -153,6 +153,7 @@ const IdentifierVerifyDialog: FC = ({ identifier, onCancel }) => { error={errorMessage} showErrorMessage={Boolean(errorMessage)} /> + {loading ? :
}
{ const { phones = [], getCurrentUser, phoneEnabled } = useCorbadoUserDetails(); const [verifyingPhones, setVerifyingPhones] = useState([]); + const [errorMessage, setErrorMessage] = useState(); const [deletingPhone, setDeletingPhone] = useState(); const [addingPhone, setAddingPhone] = useState(false); const [newPhone, setNewPhone] = useState(''); @@ -44,18 +45,20 @@ const PhonesEdit = () => { const addPhone = async () => { if (!newPhone) { - console.error('phone is empty'); + setErrorMessage(t('user-details.warning_invalid_phone')); return; } + const res = await createIdentifier(LoginIdentifierType.Phone, newPhone); if (res.err) { const code = getErrorCode(res.val.message); if (code) { - // possible code: unsupported_identifier_type (but the current UI flow should prevent this, because unsupported types are not shown) console.error(t(`errors.${code}`)); } + setErrorMessage(res.val.message); return; } + setNewPhone(''); setAddingPhone(false); void getCurrentUser(); @@ -143,12 +146,9 @@ const PhonesEdit = () => { ))} {addingPhone ? (
- setNewPhone(e.target.value)} +
{ if (item === buttonVerify) { void startEmailVerification(index); diff --git a/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx b/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx index c409fc713..b557a3d28 100644 --- a/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx +++ b/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx @@ -155,7 +155,7 @@ const IdentifierVerifyDialog: FC = ({ identifier, onCancel }) => { error={errorMessage} showErrorMessage={Boolean(errorMessage)} /> - {loading ? :
} + {loading ? :
}
{ } }; + const getMenuItems = (phone: Identifier) => { + const items = [buttonCopy]; + + if (phone.status !== 'verified') { + items.push(buttonVerify); + } + + items.push(buttonRemove); + + return items; + }; + return ( {phones.map((phone, index) => ( @@ -123,7 +135,7 @@ const PhonesEdit = () => {
{ if (item === buttonVerify) { void startPhoneVerification(index); diff --git a/packages/shared-ui/src/styles/user-details.css b/packages/shared-ui/src/styles/user-details.css index 79bcd989d..aa47f3ad2 100644 --- a/packages/shared-ui/src/styles/user-details.css +++ b/packages/shared-ui/src/styles/user-details.css @@ -89,7 +89,7 @@ padding: 0.4rem 2rem; border: 1px solid #ccc; border-radius: 0.5rem; - background-color: white; + background-color: transparent; cursor: pointer; box-sizing: border-box; font-family: var(--cb-primary-font); @@ -366,13 +366,17 @@ button.cb-user-details-deletion-dialog-secondary-button:hover { } } +.cb-otp-inputs-placeholder, .cb-otp-inputs-loader { + border: 0.25rem solid transparent; width: 20px; height: 20px; - border-color: var(--cb-primary-color) var(--cb-primary-color) var(--cb-primary-color) transparent; display: block; flex-shrink: 0; } +.cb-otp-inputs-loader { + border-color: var(--cb-primary-color) var(--cb-primary-color) var(--cb-primary-color) transparent; +} .cb-user-details-verify-identifier-container { display: flex; From 3f00b842286557136e7baf87f99e941644fda92a Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Mon, 19 Aug 2024 18:25:21 +0200 Subject: [PATCH 37/55] adds error handling for username input --- packages/react/src/screens/user-details/UsernameEdit.tsx | 9 +++++++-- packages/shared-ui/src/i18n/en.json | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/react/src/screens/user-details/UsernameEdit.tsx b/packages/react/src/screens/user-details/UsernameEdit.tsx index a1b1fcc52..8322f1ef7 100644 --- a/packages/react/src/screens/user-details/UsernameEdit.tsx +++ b/packages/react/src/screens/user-details/UsernameEdit.tsx @@ -18,6 +18,8 @@ const UsernameEdit = () => { const [addingUsername, setAddingUsername] = useState(false); const [editingUsername, setEditingUsername] = useState(false); + const [errorMessage, setErrorMessage] = useState(); + const headerUsername = useMemo(() => t('user-details.username'), [t]); const buttonSave = useMemo(() => t('user-details.save'), [t]); const buttonCancel = useMemo(() => t('user-details.cancel'), [t]); @@ -30,7 +32,7 @@ const UsernameEdit = () => { const addUsername = async () => { if (!username || !username.value) { - console.error('username is empty'); + setErrorMessage(t('user-details.username_required')); return; } const res = await createIdentifier(LoginIdentifierType.Username, username?.value || ''); @@ -48,7 +50,7 @@ const UsernameEdit = () => { const changeUsername = async () => { if (!username || !username.value) { - console.error('username is empty'); + setErrorMessage(t('user-details.username_required')); return; } const res = await updateUsername(username.id, username.value); @@ -76,6 +78,7 @@ const UsernameEdit = () => { className='cb-user-details-text' // key={`user-entry-${processUser.username}`} value={username?.value} + errorMessage={errorMessage} onChange={e => setUsername({ id: '', type: 'username', status: 'verified', value: e.target.value })} /> { onClick={() => { setUsername(undefined); setAddingUsername(false); + setErrorMessage(undefined); }} > {buttonCancel} @@ -144,6 +148,7 @@ const UsernameEdit = () => { onClick={() => { setUsername({ ...username, value: processUser.username }); setEditingUsername(false); + setErrorMessage(undefined); }} > {buttonCancel} diff --git a/packages/shared-ui/src/i18n/en.json b/packages/shared-ui/src/i18n/en.json index e4a0978f7..8d22da61d 100644 --- a/packages/shared-ui/src/i18n/en.json +++ b/packages/shared-ui/src/i18n/en.json @@ -443,6 +443,7 @@ "github": "GitHub" }, "name_required": "Please enter a valid name.", + "username_required": "Please enter a valid username.", "email_delete": { "header": "Remove Email Address", "body": "Are you sure you want to remove this email address ?", From 11ef9d51ac2b242378084d06e40c5c55e73270cf Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Wed, 21 Aug 2024 15:42:23 +0200 Subject: [PATCH 38/55] finishes error handling --- .../src/screens/user-details/EmailsEdit.tsx | 17 +++- .../user-details/IdentifierDeleteDialog.tsx | 79 ++++++++++++------- .../src/screens/user-details/PhonesEdit.tsx | 16 +++- .../src/screens/user-details/UsernameEdit.tsx | 35 +++++--- packages/shared-ui/src/i18n/en.json | 5 ++ 5 files changed, 109 insertions(+), 43 deletions(-) diff --git a/packages/react/src/screens/user-details/EmailsEdit.tsx b/packages/react/src/screens/user-details/EmailsEdit.tsx index ba16870c3..be1a32d01 100644 --- a/packages/react/src/screens/user-details/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details/EmailsEdit.tsx @@ -15,6 +15,7 @@ import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { getErrorCode, validateEmail } from '../../util'; import IdentifierDeleteDialog from './IdentifierDeleteDialog'; import IdentifierVerifyDialog from './IdentifierVerifyDialog'; +import Alert from '../../components/user-details/Alert'; const EmailsEdit = () => { const { createIdentifier, verifyIdentifierStart } = useCorbado(); @@ -25,6 +26,7 @@ const EmailsEdit = () => { const [deletingEmail, setDeletingEmail] = useState(); const [newEmail, setNewEmail] = useState(''); const [errorMessage, setErrorMessage] = useState(); + const [verifyErrorMessage, setVerifyErrorMessage] = useState<{ message: string; index: number }>(); const headerEmail = useMemo(() => t('user-details.email'), [t]); @@ -54,7 +56,9 @@ const EmailsEdit = () => { if (res.err) { const code = getErrorCode(res.val.message); if (code) { - // possible code: unsupported_identifier_type (but the current UI flow should prevent this, because unsupported types are not shown) + if (code === 'identifier_already_in_use') { + setErrorMessage(t('user-details.email_unique')); + } console.error(t(`errors.${code}`)); } return; @@ -65,12 +69,15 @@ const EmailsEdit = () => { }; const startEmailVerification = async (index: number) => { + setVerifyErrorMessage(undefined); const res = await verifyIdentifierStart(emails[index].id); if (res.err) { const code = getErrorCode(res.val.message); if (code) { - // possible code: wait_before_retry + if (code === 'wait_before_retry') { + setVerifyErrorMessage({ message: t('user-details.wait_before_retry'), index }); + } console.error(t(`errors.${code}`)); } return; @@ -152,6 +159,12 @@ const EmailsEdit = () => { getItemClassName={item => (item === buttonRemove ? 'cb-error-text-color' : '')} />
+ {verifyErrorMessage && verifyErrorMessage.index === index && ( + + )} {deletingEmail === email && ( = ({ identifier, onCancel }) => { const { t } = useTranslation('translation'); const { deleteIdentifier } = useCorbado(); const { getCurrentUser } = useCorbadoUserDetails(); + const [errorMessage, setErrorMessage] = useState(); const removeEmail = async () => { const res = await deleteIdentifier(identifier.id); if (res.err) { const code = getErrorCode(res.val.message); if (code) { - // possible codes: no_remaining_identifier, no_remaining_verified_identifier + if (code === 'no_remaining_verified_identifier' || code === 'no_remaining_identifier') { + setErrorMessage(t('user-details.no_remaining_verified_identifier')); + } console.error(t(`errors.${code}`)); } return; } void getCurrentUser(); + onCancel(); }; const getHeading = () => { @@ -67,35 +71,50 @@ const IdentifierDeleteDialog: FC = ({ identifier, onCancel }) => { return (
- - {getHeading()} - - {getBody()} + {errorMessage ? ( + <> + + + + ) : ( + <> + + {getHeading()} + + {getBody()} - -
- - -
+ + +
+ + +
+ + )}
); }; diff --git a/packages/react/src/screens/user-details/PhonesEdit.tsx b/packages/react/src/screens/user-details/PhonesEdit.tsx index 4cbce6902..4c509a833 100644 --- a/packages/react/src/screens/user-details/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details/PhonesEdit.tsx @@ -15,6 +15,7 @@ import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { getErrorCode } from '../../util'; import IdentifierDeleteDialog from './IdentifierDeleteDialog'; import IdentifierVerifyDialog from './IdentifierVerifyDialog'; +import Alert from '../../components/user-details/Alert'; const PhonesEdit = () => { const { createIdentifier, verifyIdentifierStart } = useCorbado(); @@ -25,6 +26,7 @@ const PhonesEdit = () => { const [deletingPhone, setDeletingPhone] = useState(); const [addingPhone, setAddingPhone] = useState(false); const [newPhone, setNewPhone] = useState(''); + const [verifyErrorMessage, setVerifyErrorMessage] = useState<{ message: string; index: number }>(); const headerPhone = useMemo(() => t('user-details.phone'), [t]); @@ -53,9 +55,12 @@ const PhonesEdit = () => { if (res.err) { const code = getErrorCode(res.val.message); if (code) { + if (code === 'identifier_already_in_use') { + setErrorMessage(t('user-details.phone_unique')); + } + console.error(t(`errors.${code}`)); } - setErrorMessage(res.val.message); return; } @@ -70,7 +75,8 @@ const PhonesEdit = () => { if (res.err) { const code = getErrorCode(res.val.message); if (code) { - // possible code: wait_before_retry + setVerifyErrorMessage({ message: t('user-details.wait_before_retry'), index }); + console.error(t(`errors.${code}`)); } return; @@ -146,6 +152,12 @@ const PhonesEdit = () => { getItemClassName={item => (item === buttonRemove ? 'cb-error-text-color' : '')} />
+ {verifyErrorMessage && verifyErrorMessage.index === index && ( + + )} {deletingPhone && ( { const [addingUsername, setAddingUsername] = useState(false); const [editingUsername, setEditingUsername] = useState(false); + const [newUsername, setNewUsername] = useState(username?.value); + const [errorMessage, setErrorMessage] = useState(); const headerUsername = useMemo(() => t('user-details.username'), [t]); @@ -39,7 +41,10 @@ const UsernameEdit = () => { if (res.err) { const code = getErrorCode(res.val.message); if (code) { - // possible code: unsupported_identifier_type (but the current UI flow should prevent this, because unsupported types are not shown) + if (code === 'identifier_already_in_use') { + setErrorMessage(t('user-details.username_unique')); + } + console.error(t(`errors.${code}`)); } return; @@ -49,13 +54,26 @@ const UsernameEdit = () => { }; const changeUsername = async () => { - if (!username || !username.value) { + setErrorMessage(undefined); + + if (!username || !newUsername) { setErrorMessage(t('user-details.username_required')); return; } - const res = await updateUsername(username.id, username.value); + + if (username.value === newUsername) { + setErrorMessage(t('user-details.username_unique')); + return; + } + + const res = await updateUsername(username.id, newUsername); if (res.err) { - // no possible error code + const code = getErrorCode(res.val.message); + + if (code === 'identifier_already_in_use') { + setErrorMessage(t('user-details.username_unique')); + } + console.error(res.val.message); return; } @@ -75,8 +93,7 @@ const UsernameEdit = () => {
setUsername({ id: '', type: 'username', status: 'verified', value: e.target.value })} @@ -124,10 +141,10 @@ const UsernameEdit = () => {
setUsername({ ...username, value: e.target.value })} + errorMessage={errorMessage} + onChange={e => setNewUsername(e.target.value)} /> Date: Wed, 21 Aug 2024 16:17:28 +0200 Subject: [PATCH 39/55] adds auto verification on adding new identifier --- .../src/screens/user-details/EmailsEdit.tsx | 18 ++++++++++++++++- .../src/screens/user-details/PhonesEdit.tsx | 20 +++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/react/src/screens/user-details/EmailsEdit.tsx b/packages/react/src/screens/user-details/EmailsEdit.tsx index be1a32d01..8bf1e2ca6 100644 --- a/packages/react/src/screens/user-details/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details/EmailsEdit.tsx @@ -1,7 +1,7 @@ import { LoginIdentifierType } from '@corbado/shared-ui'; import type { Identifier } from '@corbado/types'; import { t } from 'i18next'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Button, InputField, Text } from '../../components'; import { AddIcon } from '../../components/ui/icons/AddIcon'; @@ -21,6 +21,8 @@ const EmailsEdit = () => { const { createIdentifier, verifyIdentifierStart } = useCorbado(); const { emails = [], getCurrentUser, emailEnabled } = useCorbadoUserDetails(); + const initialEmails = useRef(); + const [verifyingEmails, setVerifyingEmails] = useState([]); const [addingEmail, setAddingEmail] = useState(false); const [deletingEmail, setDeletingEmail] = useState(); @@ -44,7 +46,21 @@ const EmailsEdit = () => { const buttonRemove = useMemo(() => t('user-details.remove'), [t]); useEffect(() => { + if (initialEmails.current === undefined && emails.length > 0) { + initialEmails.current = emails; + + setVerifyingEmails(new Array(emails.length).fill(false)); + return; + } setVerifyingEmails(new Array(emails.length).fill(false)); + + emails.forEach((email, index) => { + if (initialEmails.current?.every(e => e.id !== email.id)) { + void startEmailVerification(index); + } + }); + + initialEmails.current = undefined; }, [emails]); const addEmail = async () => { diff --git a/packages/react/src/screens/user-details/PhonesEdit.tsx b/packages/react/src/screens/user-details/PhonesEdit.tsx index 4c509a833..8eebdfabf 100644 --- a/packages/react/src/screens/user-details/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details/PhonesEdit.tsx @@ -1,7 +1,7 @@ import { LoginIdentifierType } from '@corbado/shared-ui'; import type { Identifier } from '@corbado/types'; import { t } from 'i18next'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Button, PhoneInputField, Text } from '../../components'; import { AddIcon } from '../../components/ui/icons/AddIcon'; @@ -21,6 +21,8 @@ const PhonesEdit = () => { const { createIdentifier, verifyIdentifierStart } = useCorbado(); const { phones = [], getCurrentUser, phoneEnabled } = useCorbadoUserDetails(); + const initialPhones = useRef(); + const [verifyingPhones, setVerifyingPhones] = useState([]); const [errorMessage, setErrorMessage] = useState(); const [deletingPhone, setDeletingPhone] = useState(); @@ -42,7 +44,21 @@ const PhonesEdit = () => { const buttonRemove = useMemo(() => t('user-details.remove'), [t]); useEffect(() => { + if (initialPhones.current === undefined && phones.length > 0) { + initialPhones.current = phones; + + setVerifyingPhones(new Array(phones.length).fill(false)); + return; + } setVerifyingPhones(new Array(phones.length).fill(false)); + + phones.forEach((email, index) => { + if (initialPhones.current?.every(e => e.id !== email.id)) { + void startPhoneVerification(index); + } + }); + + initialPhones.current = undefined; }, [phones]); const addPhone = async () => { @@ -120,7 +136,7 @@ const PhonesEdit = () => { return ( - {phones.map((phone, index) => ( + {phones.reverse().map((phone, index) => (
Date: Wed, 21 Aug 2024 16:58:57 +0200 Subject: [PATCH 40/55] submit on enter --- .../src/screens/user-details/EmailsEdit.tsx | 9 +++++-- .../src/screens/user-details/NameEdit.tsx | 25 +++++++++++++------ .../src/screens/user-details/PhonesEdit.tsx | 17 ++++++++++--- .../src/screens/user-details/UsernameEdit.tsx | 16 +++++++++--- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/packages/react/src/screens/user-details/EmailsEdit.tsx b/packages/react/src/screens/user-details/EmailsEdit.tsx index 8bf1e2ca6..792596114 100644 --- a/packages/react/src/screens/user-details/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details/EmailsEdit.tsx @@ -192,7 +192,10 @@ const EmailsEdit = () => {
))} {addingEmail ? ( -
+
e.preventDefault()} + className='cb-user-details-identifier-container' + > { errorMessage={errorMessage} /> -
+ ) : ( ) : ( -
+
{ + e.preventDefault(); + }} + >
setName(e.target.value)} @@ -91,11 +95,15 @@ const NameEdit: FC = () => {
)} -
+ )} ); diff --git a/packages/react/src/screens/user-details/PhonesEdit.tsx b/packages/react/src/screens/user-details/PhonesEdit.tsx index 8eebdfabf..ad036f1a8 100644 --- a/packages/react/src/screens/user-details/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details/PhonesEdit.tsx @@ -134,6 +134,10 @@ const PhonesEdit = () => { return items; }; + const copyPhone = async (phone: string) => { + await navigator.clipboard.writeText(phone); + }; + return ( {phones.reverse().map((phone, index) => ( @@ -163,6 +167,8 @@ const PhonesEdit = () => { void startPhoneVerification(index); } else if (item === buttonRemove) { setDeletingPhone(phone); + } else { + void copyPhone(phone.value); } }} getItemClassName={item => (item === buttonRemove ? 'cb-error-text-color' : '')} @@ -174,7 +180,7 @@ const PhonesEdit = () => { text={verifyErrorMessage.message} /> )} - {deletingPhone && ( + {deletingPhone === phone && ( setDeletingPhone(undefined)} @@ -185,18 +191,23 @@ const PhonesEdit = () => {
))} {addingPhone ? ( -
+
e.preventDefault()} + className='cb-user-details-identifier-container' + > -
+ ) : ( -
+ ) : ( )} -
+ )}
)} From 175275364416aef641301929b890889192c9db3a Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Wed, 21 Aug 2024 17:29:10 +0200 Subject: [PATCH 41/55] fixes lint issues --- packages/react/src/screens/user-details/EmailsEdit.tsx | 2 +- packages/react/src/screens/user-details/PhonesEdit.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/screens/user-details/EmailsEdit.tsx b/packages/react/src/screens/user-details/EmailsEdit.tsx index 792596114..784b74b56 100644 --- a/packages/react/src/screens/user-details/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details/EmailsEdit.tsx @@ -8,6 +8,7 @@ import { AddIcon } from '../../components/ui/icons/AddIcon'; import { PendingIcon } from '../../components/ui/icons/PendingIcon'; import { PrimaryIcon } from '../../components/ui/icons/PrimaryIcon'; import { VerifiedIcon } from '../../components/ui/icons/VerifiedIcon'; +import Alert from '../../components/user-details/Alert'; import DropdownMenu from '../../components/user-details/DropdownMenu'; import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; @@ -15,7 +16,6 @@ import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { getErrorCode, validateEmail } from '../../util'; import IdentifierDeleteDialog from './IdentifierDeleteDialog'; import IdentifierVerifyDialog from './IdentifierVerifyDialog'; -import Alert from '../../components/user-details/Alert'; const EmailsEdit = () => { const { createIdentifier, verifyIdentifierStart } = useCorbado(); diff --git a/packages/react/src/screens/user-details/PhonesEdit.tsx b/packages/react/src/screens/user-details/PhonesEdit.tsx index ad036f1a8..3fad9526e 100644 --- a/packages/react/src/screens/user-details/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details/PhonesEdit.tsx @@ -8,6 +8,7 @@ import { AddIcon } from '../../components/ui/icons/AddIcon'; import { PendingIcon } from '../../components/ui/icons/PendingIcon'; import { PrimaryIcon } from '../../components/ui/icons/PrimaryIcon'; import { VerifiedIcon } from '../../components/ui/icons/VerifiedIcon'; +import Alert from '../../components/user-details/Alert'; import DropdownMenu from '../../components/user-details/DropdownMenu'; import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; @@ -15,7 +16,6 @@ import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { getErrorCode } from '../../util'; import IdentifierDeleteDialog from './IdentifierDeleteDialog'; import IdentifierVerifyDialog from './IdentifierVerifyDialog'; -import Alert from '../../components/user-details/Alert'; const PhonesEdit = () => { const { createIdentifier, verifyIdentifierStart } = useCorbado(); From 55595884192d589634816e10facad765f06b8503 Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Wed, 21 Aug 2024 17:43:14 +0200 Subject: [PATCH 42/55] adds missing dark variables --- packages/shared-ui/src/styles/variables.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/shared-ui/src/styles/variables.css b/packages/shared-ui/src/styles/variables.css index a77e3d782..37e4afeb1 100644 --- a/packages/shared-ui/src/styles/variables.css +++ b/packages/shared-ui/src/styles/variables.css @@ -67,6 +67,8 @@ --cb-passkey-list-description-text-color: #d9d9d9; --cb-passkey-list-border-color: rgb(132 140 166 / 65%); --cb-passkey-list-border-hover-color: #ffffff; + --cb-user-details-dialog-background-color: #00000; + --cb-user-details-dialog-border-color: rgba(132, 140, 166, 0.65); } @media screen and (max-width: 481px) { From 5045d107f53f660bd009cdba7f761678e467cba1 Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Thu, 22 Aug 2024 11:16:39 +0200 Subject: [PATCH 43/55] resolves comments --- .../components/user-details/DropdownMenu.tsx | 7 ++- .../user-details/IdentifierVerifyDialog.tsx | 10 +++- packages/shared-ui/src/styles/inputs.css | 1 + .../shared-ui/src/styles/user-details.css | 46 +++++++++++++------ packages/shared-ui/src/styles/variables.css | 1 + 5 files changed, 49 insertions(+), 16 deletions(-) diff --git a/packages/react/src/components/user-details/DropdownMenu.tsx b/packages/react/src/components/user-details/DropdownMenu.tsx index 7c9ad7fd8..725600f77 100644 --- a/packages/react/src/components/user-details/DropdownMenu.tsx +++ b/packages/react/src/components/user-details/DropdownMenu.tsx @@ -48,7 +48,12 @@ const DropdownMenu: FC = ({ items, onItemClick, getItemClassName }) => { }} className='cb-dropdown-menu-item' > - {item} + + {item} +
))}
diff --git a/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx b/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx index b557a3d28..58ff6ec2f 100644 --- a/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx +++ b/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx @@ -155,7 +155,13 @@ const IdentifierVerifyDialog: FC = ({ identifier, onCancel }) => { error={errorMessage} showErrorMessage={Boolean(errorMessage)} /> - {loading ? :
} + {loading ? ( +
+ +
+ ) : ( +
+ )}
= ({ identifier, onCancel }) => { onClick={onCancel} > diff --git a/packages/shared-ui/src/styles/inputs.css b/packages/shared-ui/src/styles/inputs.css index bdb1061d6..45c61e647 100644 --- a/packages/shared-ui/src/styles/inputs.css +++ b/packages/shared-ui/src/styles/inputs.css @@ -178,6 +178,7 @@ border: none; margin-bottom: 0.5rem; width: 98%; + padding: 0.5rem; } .cb-phone-input-field-search:focus { diff --git a/packages/shared-ui/src/styles/user-details.css b/packages/shared-ui/src/styles/user-details.css index aa47f3ad2..d46d6e440 100644 --- a/packages/shared-ui/src/styles/user-details.css +++ b/packages/shared-ui/src/styles/user-details.css @@ -93,6 +93,10 @@ cursor: pointer; box-sizing: border-box; font-family: var(--cb-primary-font); + + &:hover { + background-color: var(--cb-box-color-hover); + } } .cb-user-details-body-button-icon { @@ -112,7 +116,10 @@ span { color: var(--cb-button-text-primary-color); - font-size: 13px; + } + + &:hover { + background-color: var(--cb-primary-color-hover); } } @@ -122,10 +129,15 @@ padding: 0.4rem 3rem; background-color: var(--cb-secondary-color); border: 1px solid transparent; - border-radius: var(--cb-border-radius); + border-radius: var(--cb-border-radius-sm); cursor: pointer; box-sizing: border-box; font-family: var(--cb-primary-font); + margin-left: 0.5rem; + + &:hover { + background-color: var(--cb-box-color-hover); + } } .cb-user-details-verify-identifier-button-cancel { @@ -135,7 +147,6 @@ .cb-user-details-body-button-secondary span { color: var(--cb-secondary-link-color); - font-size: 10px; } .cb-user-details-body-button-delete { @@ -147,6 +158,7 @@ cursor: pointer; box-sizing: border-box; font-family: var(--cb-primary-font); + padding-right: 1rem; } .cb-user-details-identifier-container { @@ -279,17 +291,18 @@ } .cb-dropdown-menu-item { - padding: 0.3rem 0.5rem; + padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--cb-border-color); cursor: pointer; -} -.cb-dropdown-menu-item:hover { - cursor: pointer; -} + &:hover { + background-color: var(--cb-box-color-hover); + cursor: pointer; + } -.cb-dropdown-menu-item:last-child { - border-bottom: none; + &:last-child { + border-bottom: none; + } } .cb-dropdown-menu-trigger { @@ -338,11 +351,11 @@ button.cb-user-details-deletion-dialog-secondary-button { } button.cb-user-details-deletion-dialog-primary-button:hover { - background-color: var(--cb-error-color); + background-color: var(--cb-error-color-hover); } button.cb-user-details-deletion-dialog-secondary-button:hover { - background-color: transparent; + background-color: var(--cb-box-color-hover); } .cb-user-details-deletion-dialog-cta { @@ -357,7 +370,6 @@ button.cb-user-details-deletion-dialog-secondary-button:hover { gap: 0.5rem; margin-bottom: 1rem; width: 100%; - align-items: center; max-width: 340px; margin-top: 0.5rem; @@ -374,10 +386,18 @@ button.cb-user-details-deletion-dialog-secondary-button:hover { display: block; flex-shrink: 0; } + .cb-otp-inputs-loader { border-color: var(--cb-primary-color) var(--cb-primary-color) var(--cb-primary-color) transparent; } +.cb-otp-inputs-loader-container { + margin-top: 0.3rem; + display: flex; + justify-items: center; + height: 52px; +} + .cb-user-details-verify-identifier-container { display: flex; flex-direction: column; diff --git a/packages/shared-ui/src/styles/variables.css b/packages/shared-ui/src/styles/variables.css index 37e4afeb1..fbf751383 100644 --- a/packages/shared-ui/src/styles/variables.css +++ b/packages/shared-ui/src/styles/variables.css @@ -17,6 +17,7 @@ --cb-box-color-hover: #f5f5f5; --cb-error-text-color: #fa5f55; --cb-error-color: #d51d1d; + --cb-error-color-hover: #e65252; --cb-otp-disabled-color: #fbfbfb; --cb-divider-line-color: rgba(0, 0, 0, 0.3); --cb-input-disabled-color: #dfdfe0; From cd5bd4567f0f98c5a4a6db1c5b5cbcda5ffb6472 Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Thu, 22 Aug 2024 14:02:44 +0200 Subject: [PATCH 44/55] adds copy button effect --- .../src/components/ui/buttons/CopyButton.tsx | 58 +++++++++++++++++++ .../src/components/ui/icons/TickIcon.tsx | 22 +++++++ .../src/screens/user-details/NameEdit.tsx | 16 ++--- .../src/screens/user-details/UsernameEdit.tsx | 13 ++--- packages/shared-ui/src/assets/tick.svg | 4 ++ packages/shared-ui/src/i18n/en.json | 1 + packages/shared-ui/src/index.ts | 2 + .../shared-ui/src/styles/user-details.css | 20 ++++++- packages/shared-ui/src/styles/variables.css | 1 + 9 files changed, 116 insertions(+), 21 deletions(-) create mode 100644 packages/react/src/components/ui/buttons/CopyButton.tsx create mode 100644 packages/react/src/components/ui/icons/TickIcon.tsx create mode 100644 packages/shared-ui/src/assets/tick.svg diff --git a/packages/react/src/components/ui/buttons/CopyButton.tsx b/packages/react/src/components/ui/buttons/CopyButton.tsx new file mode 100644 index 000000000..6583e52ce --- /dev/null +++ b/packages/react/src/components/ui/buttons/CopyButton.tsx @@ -0,0 +1,58 @@ +import React, { FC, useEffect, useState } from 'react'; +import { CopyIcon } from '../icons/CopyIcon'; +import { TickIcon } from '../icons/TickIcon'; +import { useTranslation } from 'react-i18next'; +import { Text } from '../typography'; + +interface Props { + text: string | undefined; +} + +const RESET_TIMEOUT = 4 * 1000; + +const CopyButton: FC = ({ text }) => { + const { t } = useTranslation(); + const [copied, setCopied] = useState(false); + + useEffect(() => { + const reset = () => { + setCopied(false); + }; + + if (copied) { + const timeout = setTimeout(reset, RESET_TIMEOUT); + + return () => clearTimeout(timeout); + } + + return; + }, [copied]); + + const onClick = async () => { + if (text) { + await navigator.clipboard.writeText(text); + setCopied(true); + } + }; + + if (copied) { + return ( +
+ +
+ {t('user-details.copied')} +
+
+ ); + } + + return ( + void onClick()} + /> + ); +}; + +export default CopyButton; diff --git a/packages/react/src/components/ui/icons/TickIcon.tsx b/packages/react/src/components/ui/icons/TickIcon.tsx new file mode 100644 index 000000000..d748bfdae --- /dev/null +++ b/packages/react/src/components/ui/icons/TickIcon.tsx @@ -0,0 +1,22 @@ +import syncIconSrc from '@corbado/shared-ui/assets/tick.svg'; +import type { FC } from 'react'; +import { memo, useRef } from 'react'; +import React from 'react'; + +import { ColorType, useIconWithTheme } from '../../../hooks/useIconWithTheme'; +import type { IconProps } from './Icon'; +import { Icon } from './Icon'; + +export const TickIcon: FC = memo(props => { + const svgRef = useRef(null); + const { logoSVG } = useIconWithTheme(svgRef, syncIconSrc, '--cb-success-color', ColorType.Stroke); + + return ( + + ); +}); diff --git a/packages/react/src/screens/user-details/NameEdit.tsx b/packages/react/src/screens/user-details/NameEdit.tsx index a002a4b87..d6d60d419 100644 --- a/packages/react/src/screens/user-details/NameEdit.tsx +++ b/packages/react/src/screens/user-details/NameEdit.tsx @@ -5,10 +5,10 @@ import { useTranslation } from 'react-i18next'; import { Button, InputField, Text } from '../../components'; import { AddIcon } from '../../components/ui/icons/AddIcon'; import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; -import { CopyIcon } from '../../components/ui/icons/CopyIcon'; import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; +import CopyButton from '../../components/ui/buttons/CopyButton'; const NameEdit: FC = () => { const { updateFullName } = useCorbado(); @@ -26,13 +26,9 @@ const NameEdit: FC = () => { const [errorMessage, setErrorMessage] = useState(undefined); - const copyName = async () => { - if (name) { - await navigator.clipboard.writeText(name); - } - }; - const changeName = async () => { + setErrorMessage(undefined); + if (!name) { setErrorMessage(t('user-details.name_required')); return; @@ -85,11 +81,7 @@ const NameEdit: FC = () => { onChange={e => setName(e.target.value)} errorMessage={errorMessage} /> - void copyName()} - /> +
{editingName ? (
diff --git a/packages/react/src/screens/user-details/UsernameEdit.tsx b/packages/react/src/screens/user-details/UsernameEdit.tsx index d1f2ad71d..595098a33 100644 --- a/packages/react/src/screens/user-details/UsernameEdit.tsx +++ b/packages/react/src/screens/user-details/UsernameEdit.tsx @@ -10,6 +10,7 @@ import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { getErrorCode } from '../../util'; +import CopyButton from '../../components/ui/buttons/CopyButton'; const UsernameEdit = () => { const { createIdentifier, updateUsername } = useCorbado(); @@ -33,6 +34,8 @@ const UsernameEdit = () => { }; const addUsername = async () => { + setErrorMessage(undefined); + if (!username || !username.value) { setErrorMessage(t('user-details.username_required')); return; @@ -146,16 +149,12 @@ const UsernameEdit = () => {
setNewUsername(e.target.value)} /> - void copyUsername()} - /> +
{editingUsername ? (
@@ -170,7 +169,7 @@ const UsernameEdit = () => { className='cb-user-details-body-button-secondary' type='button' onClick={() => { - setUsername({ ...username, value: processUser.username }); + setNewUsername(username.value); setEditingUsername(false); setErrorMessage(undefined); }} diff --git a/packages/shared-ui/src/assets/tick.svg b/packages/shared-ui/src/assets/tick.svg new file mode 100644 index 000000000..3fa4f7705 --- /dev/null +++ b/packages/shared-ui/src/assets/tick.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/packages/shared-ui/src/i18n/en.json b/packages/shared-ui/src/i18n/en.json index 127d0058b..9e45fed5e 100644 --- a/packages/shared-ui/src/i18n/en.json +++ b/packages/shared-ui/src/i18n/en.json @@ -408,6 +408,7 @@ } }, "user-details": { + "copied":"Copied", "title": "User Details", "name": "Name", "username": "Username", diff --git a/packages/shared-ui/src/index.ts b/packages/shared-ui/src/index.ts index 5b303c6de..c5b053864 100644 --- a/packages/shared-ui/src/index.ts +++ b/packages/shared-ui/src/index.ts @@ -48,6 +48,7 @@ import syncIcon from './assets/sync.svg'; import verifiedIcon from './assets/verified.svg'; import visibilityIcon from './assets/visibility.svg'; import yahooIcon from './assets/yahoo.svg'; +import tickIcon from './assets/tick.svg'; import i18nDe from './i18n/de.json'; import i18nEn from './i18n/en.json'; @@ -108,4 +109,5 @@ export const assets = { primaryIcon, verifiedIcon, pendingIcon, + tickIcon, }; diff --git a/packages/shared-ui/src/styles/user-details.css b/packages/shared-ui/src/styles/user-details.css index d46d6e440..ba7889a76 100644 --- a/packages/shared-ui/src/styles/user-details.css +++ b/packages/shared-ui/src/styles/user-details.css @@ -78,8 +78,7 @@ .cb-user-details-body-row-icon { height: 1.5rem; cursor: pointer; - margin-left: 0.5rem; - margin-top: 0.45rem; + padding: 0.5rem 0.4rem; align-self: flex-start; } @@ -404,6 +403,23 @@ button.cb-user-details-deletion-dialog-secondary-button:hover { gap: 6px; } +.cb-tooltip-container { + position: relative; + display: inline-block; +} + +.cb-tooltip { + position: absolute; + bottom: -20px; + margin-top: 0.1rem; + left: 50%; + transform: translate(-50%, 0); + border-radius: var(--cb-border-radius-sm); + background-color: var(--cb-box-color); + box-shadow: 0px 2px 6px 2px var(--cb-border-color); + padding: 0.2rem 0.4rem; +} + @media screen and (max-width: 481px) { .cb-user-details-section-indentifier { width: 75%; diff --git a/packages/shared-ui/src/styles/variables.css b/packages/shared-ui/src/styles/variables.css index fbf751383..67365a3d2 100644 --- a/packages/shared-ui/src/styles/variables.css +++ b/packages/shared-ui/src/styles/variables.css @@ -17,6 +17,7 @@ --cb-box-color-hover: #f5f5f5; --cb-error-text-color: #fa5f55; --cb-error-color: #d51d1d; + --cb-success-color: #79d51d; --cb-error-color-hover: #e65252; --cb-otp-disabled-color: #fbfbfb; --cb-divider-line-color: rgba(0, 0, 0, 0.3); From d1bce55471f2caeb9e9c378b0053efc1912fe6f8 Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Thu, 22 Aug 2024 14:24:54 +0200 Subject: [PATCH 45/55] fixes text overflow in identifiers --- packages/shared-ui/src/styles/user-details.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/shared-ui/src/styles/user-details.css b/packages/shared-ui/src/styles/user-details.css index ba7889a76..dbd5c7fad 100644 --- a/packages/shared-ui/src/styles/user-details.css +++ b/packages/shared-ui/src/styles/user-details.css @@ -174,6 +174,14 @@ align-items: center; flex-wrap: wrap; gap: 0.5rem; + overflow: hidden; + + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 300px; + } } .cb-user-details-header-badge { From 6430923ad83904c8a0f881fc1631cf7d91ea40ca Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Thu, 22 Aug 2024 15:28:45 +0200 Subject: [PATCH 46/55] fixes verification timeout --- .../src/screens/user-details/EmailsEdit.tsx | 46 +++++-------------- .../user-details/IdentifierVerifyDialog.tsx | 9 ++-- .../src/screens/user-details/PhonesEdit.tsx | 46 +++++-------------- packages/shared-ui/src/i18n/en.json | 1 - packages/shared-ui/src/styles/typography.css | 6 +++ 5 files changed, 35 insertions(+), 73 deletions(-) diff --git a/packages/react/src/screens/user-details/EmailsEdit.tsx b/packages/react/src/screens/user-details/EmailsEdit.tsx index 784b74b56..0e82692f2 100644 --- a/packages/react/src/screens/user-details/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details/EmailsEdit.tsx @@ -8,7 +8,6 @@ import { AddIcon } from '../../components/ui/icons/AddIcon'; import { PendingIcon } from '../../components/ui/icons/PendingIcon'; import { PrimaryIcon } from '../../components/ui/icons/PrimaryIcon'; import { VerifiedIcon } from '../../components/ui/icons/VerifiedIcon'; -import Alert from '../../components/user-details/Alert'; import DropdownMenu from '../../components/user-details/DropdownMenu'; import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; @@ -18,17 +17,16 @@ import IdentifierDeleteDialog from './IdentifierDeleteDialog'; import IdentifierVerifyDialog from './IdentifierVerifyDialog'; const EmailsEdit = () => { - const { createIdentifier, verifyIdentifierStart } = useCorbado(); + const { createIdentifier } = useCorbado(); const { emails = [], getCurrentUser, emailEnabled } = useCorbadoUserDetails(); const initialEmails = useRef(); - const [verifyingEmails, setVerifyingEmails] = useState([]); + const [verifyingEmails, setVerifyingEmails] = useState([]); const [addingEmail, setAddingEmail] = useState(false); const [deletingEmail, setDeletingEmail] = useState(); const [newEmail, setNewEmail] = useState(''); const [errorMessage, setErrorMessage] = useState(); - const [verifyErrorMessage, setVerifyErrorMessage] = useState<{ message: string; index: number }>(); const headerEmail = useMemo(() => t('user-details.email'), [t]); @@ -49,14 +47,12 @@ const EmailsEdit = () => { if (initialEmails.current === undefined && emails.length > 0) { initialEmails.current = emails; - setVerifyingEmails(new Array(emails.length).fill(false)); return; } - setVerifyingEmails(new Array(emails.length).fill(false)); - emails.forEach((email, index) => { + emails.forEach(email => { if (initialEmails.current?.every(e => e.id !== email.id)) { - void startEmailVerification(index); + setVerifyingEmails(prev => [...prev, email]); } }); @@ -84,26 +80,12 @@ const EmailsEdit = () => { void getCurrentUser(); }; - const startEmailVerification = async (index: number) => { - setVerifyErrorMessage(undefined); - const res = await verifyIdentifierStart(emails[index].id); - - if (res.err) { - const code = getErrorCode(res.val.message); - if (code) { - if (code === 'wait_before_retry') { - setVerifyErrorMessage({ message: t('user-details.wait_before_retry'), index }); - } - console.error(t(`errors.${code}`)); - } - return; - } - - setVerifyingEmails(prev => prev.map((v, i) => (i === index ? true : v))); + const startEmailVerification = async (email: Identifier) => { + setVerifyingEmails(prev => [...prev, email]); }; - const onFinishEmailVerification = (index: number) => { - setVerifyingEmails(prev => prev.map((v, i) => (i === index ? false : v))); + const onFinishEmailVerification = (email: Identifier) => { + setVerifyingEmails(prev => prev.filter(v => v.id !== email.id)); }; if (!emailEnabled) { @@ -146,10 +128,10 @@ const EmailsEdit = () => { className='cb-user-details-identifier-container' key={index} > - {verifyingEmails[index] ? ( + {verifyingEmails.some(verifyingEmail => verifyingEmail.id === email.id) ? ( onFinishEmailVerification(index)} + onCancel={() => onFinishEmailVerification(email)} /> ) : ( <> @@ -165,7 +147,7 @@ const EmailsEdit = () => { items={getMenuItems(email)} onItemClick={item => { if (item === buttonVerify) { - void startEmailVerification(index); + void startEmailVerification(email); } else if (item === buttonRemove) { setDeletingEmail(email); } else { @@ -175,12 +157,6 @@ const EmailsEdit = () => { getItemClassName={item => (item === buttonRemove ? 'cb-error-text-color' : '')} />
- {verifyErrorMessage && verifyErrorMessage.index === index && ( - - )} {deletingEmail === email && ( = ({ identifier, onCancel }) => { useEffect(() => { setLoading(false); + resendEmailVerification(); const timer = startTimer(); @@ -107,19 +108,20 @@ const IdentifierVerifyDialog: FC = ({ identifier, onCancel }) => { void finishEmailVerification(otp).finally(() => setLoading(false)); }, []); - const resendEmailVerification = async (e: MouseEvent) => { - e.preventDefault(); + const resendEmailVerification = async (e?: MouseEvent) => { + e?.preventDefault(); if (remainingTime > 0) { return; } + setErrorMessage(undefined); + const res = await verifyIdentifierStart(identifier.id); if (res.err) { const code = getErrorCode(res.val.message); if (code) { - // possible code: invalid_challenge_solution_email-otp console.error(t(`errors.${code}`)); } return; @@ -139,6 +141,7 @@ const IdentifierVerifyDialog: FC = ({ identifier, onCancel }) => { {identifier.value} diff --git a/packages/react/src/screens/user-details/PhonesEdit.tsx b/packages/react/src/screens/user-details/PhonesEdit.tsx index 3fad9526e..cda328ec6 100644 --- a/packages/react/src/screens/user-details/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details/PhonesEdit.tsx @@ -8,7 +8,6 @@ import { AddIcon } from '../../components/ui/icons/AddIcon'; import { PendingIcon } from '../../components/ui/icons/PendingIcon'; import { PrimaryIcon } from '../../components/ui/icons/PrimaryIcon'; import { VerifiedIcon } from '../../components/ui/icons/VerifiedIcon'; -import Alert from '../../components/user-details/Alert'; import DropdownMenu from '../../components/user-details/DropdownMenu'; import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; @@ -18,17 +17,16 @@ import IdentifierDeleteDialog from './IdentifierDeleteDialog'; import IdentifierVerifyDialog from './IdentifierVerifyDialog'; const PhonesEdit = () => { - const { createIdentifier, verifyIdentifierStart } = useCorbado(); + const { createIdentifier } = useCorbado(); const { phones = [], getCurrentUser, phoneEnabled } = useCorbadoUserDetails(); const initialPhones = useRef(); - const [verifyingPhones, setVerifyingPhones] = useState([]); + const [verifyingPhones, setVerifyingPhones] = useState([]); const [errorMessage, setErrorMessage] = useState(); const [deletingPhone, setDeletingPhone] = useState(); const [addingPhone, setAddingPhone] = useState(false); const [newPhone, setNewPhone] = useState(''); - const [verifyErrorMessage, setVerifyErrorMessage] = useState<{ message: string; index: number }>(); const headerPhone = useMemo(() => t('user-details.phone'), [t]); @@ -47,14 +45,12 @@ const PhonesEdit = () => { if (initialPhones.current === undefined && phones.length > 0) { initialPhones.current = phones; - setVerifyingPhones(new Array(phones.length).fill(false)); return; } - setVerifyingPhones(new Array(phones.length).fill(false)); - phones.forEach((email, index) => { - if (initialPhones.current?.every(e => e.id !== email.id)) { - void startPhoneVerification(index); + phones.forEach(phone => { + if (initialPhones.current?.every(p => p.id !== phone.id)) { + setVerifyingPhones(prev => [...prev, phone]); } }); @@ -85,24 +81,12 @@ const PhonesEdit = () => { void getCurrentUser(); }; - const startPhoneVerification = async (index: number) => { - const res = await verifyIdentifierStart(phones[index].id); - - if (res.err) { - const code = getErrorCode(res.val.message); - if (code) { - setVerifyErrorMessage({ message: t('user-details.wait_before_retry'), index }); - - console.error(t(`errors.${code}`)); - } - return; - } - - setVerifyingPhones(prev => prev.map((v, i) => (i === index ? true : v))); + const startPhoneVerification = async (phone: Identifier) => { + setVerifyingPhones(prev => [...prev, phone]); }; - const onFinishEmailVerification = (index: number) => { - setVerifyingPhones(prev => prev.map((v, i) => (i === index ? false : v))); + const onFinishPhoneVerification = (phone: Identifier) => { + setVerifyingPhones(prev => prev.filter(v => v.id !== phone.id)); }; if (!phoneEnabled) { @@ -145,10 +129,10 @@ const PhonesEdit = () => { className='cb-user-details-identifier-container' key={index} > - {verifyingPhones[index] ? ( + {verifyingPhones.some(verifyingPhone => verifyingPhone.id === phone.id) ? ( onFinishEmailVerification(index)} + onCancel={() => onFinishPhoneVerification(phone)} /> ) : ( <> @@ -164,7 +148,7 @@ const PhonesEdit = () => { items={getMenuItems(phone)} onItemClick={item => { if (item === buttonVerify) { - void startPhoneVerification(index); + void startPhoneVerification(phone); } else if (item === buttonRemove) { setDeletingPhone(phone); } else { @@ -174,12 +158,6 @@ const PhonesEdit = () => { getItemClassName={item => (item === buttonRemove ? 'cb-error-text-color' : '')} />
- {verifyErrorMessage && verifyErrorMessage.index === index && ( - - )} {deletingPhone === phone && ( Date: Thu, 22 Aug 2024 15:33:07 +0200 Subject: [PATCH 47/55] fixes lint issues --- packages/react/src/components/ui/buttons/CopyButton.tsx | 6 ++++-- packages/react/src/screens/user-details/EmailsEdit.tsx | 2 +- .../src/screens/user-details/IdentifierVerifyDialog.tsx | 3 ++- packages/react/src/screens/user-details/NameEdit.tsx | 2 +- packages/react/src/screens/user-details/PhonesEdit.tsx | 2 +- packages/react/src/screens/user-details/UsernameEdit.tsx | 2 +- packages/shared-ui/src/i18n/en.json | 2 +- packages/shared-ui/src/index.ts | 2 +- 8 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/react/src/components/ui/buttons/CopyButton.tsx b/packages/react/src/components/ui/buttons/CopyButton.tsx index 6583e52ce..f23b6a360 100644 --- a/packages/react/src/components/ui/buttons/CopyButton.tsx +++ b/packages/react/src/components/ui/buttons/CopyButton.tsx @@ -1,7 +1,9 @@ -import React, { FC, useEffect, useState } from 'react'; +import type { FC } from 'react'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + import { CopyIcon } from '../icons/CopyIcon'; import { TickIcon } from '../icons/TickIcon'; -import { useTranslation } from 'react-i18next'; import { Text } from '../typography'; interface Props { diff --git a/packages/react/src/screens/user-details/EmailsEdit.tsx b/packages/react/src/screens/user-details/EmailsEdit.tsx index 0e82692f2..be88d0e23 100644 --- a/packages/react/src/screens/user-details/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details/EmailsEdit.tsx @@ -80,7 +80,7 @@ const EmailsEdit = () => { void getCurrentUser(); }; - const startEmailVerification = async (email: Identifier) => { + const startEmailVerification = (email: Identifier) => { setVerifyingEmails(prev => [...prev, email]); }; diff --git a/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx b/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx index 64fee29be..fbf8d0e07 100644 --- a/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx +++ b/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx @@ -60,7 +60,8 @@ const IdentifierVerifyDialog: FC = ({ identifier, onCancel }) => { useEffect(() => { setLoading(false); - resendEmailVerification(); + + void resendEmailVerification(); const timer = startTimer(); diff --git a/packages/react/src/screens/user-details/NameEdit.tsx b/packages/react/src/screens/user-details/NameEdit.tsx index d6d60d419..08235a5ae 100644 --- a/packages/react/src/screens/user-details/NameEdit.tsx +++ b/packages/react/src/screens/user-details/NameEdit.tsx @@ -3,12 +3,12 @@ import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, InputField, Text } from '../../components'; +import CopyButton from '../../components/ui/buttons/CopyButton'; import { AddIcon } from '../../components/ui/icons/AddIcon'; import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; -import CopyButton from '../../components/ui/buttons/CopyButton'; const NameEdit: FC = () => { const { updateFullName } = useCorbado(); diff --git a/packages/react/src/screens/user-details/PhonesEdit.tsx b/packages/react/src/screens/user-details/PhonesEdit.tsx index cda328ec6..aca2261b8 100644 --- a/packages/react/src/screens/user-details/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details/PhonesEdit.tsx @@ -81,7 +81,7 @@ const PhonesEdit = () => { void getCurrentUser(); }; - const startPhoneVerification = async (phone: Identifier) => { + const startPhoneVerification = (phone: Identifier) => { setVerifyingPhones(prev => [...prev, phone]); }; diff --git a/packages/react/src/screens/user-details/UsernameEdit.tsx b/packages/react/src/screens/user-details/UsernameEdit.tsx index 595098a33..22440dfab 100644 --- a/packages/react/src/screens/user-details/UsernameEdit.tsx +++ b/packages/react/src/screens/user-details/UsernameEdit.tsx @@ -3,6 +3,7 @@ import { t } from 'i18next'; import React, { useMemo, useState } from 'react'; import { Button, InputField, Text } from '../../components'; +import CopyButton from '../../components/ui/buttons/CopyButton'; import { AddIcon } from '../../components/ui/icons/AddIcon'; import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; import { CopyIcon } from '../../components/ui/icons/CopyIcon'; @@ -10,7 +11,6 @@ import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; import { getErrorCode } from '../../util'; -import CopyButton from '../../components/ui/buttons/CopyButton'; const UsernameEdit = () => { const { createIdentifier, updateUsername } = useCorbado(); diff --git a/packages/shared-ui/src/i18n/en.json b/packages/shared-ui/src/i18n/en.json index 5260d89d5..b57886055 100644 --- a/packages/shared-ui/src/i18n/en.json +++ b/packages/shared-ui/src/i18n/en.json @@ -408,7 +408,7 @@ } }, "user-details": { - "copied":"Copied", + "copied": "Copied", "title": "User Details", "name": "Name", "username": "Username", diff --git a/packages/shared-ui/src/index.ts b/packages/shared-ui/src/index.ts index c5b053864..78753e399 100644 --- a/packages/shared-ui/src/index.ts +++ b/packages/shared-ui/src/index.ts @@ -45,10 +45,10 @@ import rightIcon from './assets/right-arrow.svg'; import secureIcon from './assets/secure-icon.svg'; import shieldIcon from './assets/shield.svg'; import syncIcon from './assets/sync.svg'; +import tickIcon from './assets/tick.svg'; import verifiedIcon from './assets/verified.svg'; import visibilityIcon from './assets/visibility.svg'; import yahooIcon from './assets/yahoo.svg'; -import tickIcon from './assets/tick.svg'; import i18nDe from './i18n/de.json'; import i18nEn from './i18n/en.json'; From 6641e261f98befb34a1bfd44bbfceea9e91257e9 Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Thu, 22 Aug 2024 17:15:07 +0200 Subject: [PATCH 48/55] adds loading states to buttons --- .../src/components/ui/buttons/Button.tsx | 7 ++++- .../src/screens/user-details/EmailsEdit.tsx | 19 +++++++++++--- .../src/screens/user-details/NameEdit.tsx | 16 ++++++++++-- .../src/screens/user-details/PhonesEdit.tsx | 17 +++++++++--- .../src/screens/user-details/UsernameEdit.tsx | 26 ++++++++++++++++--- packages/shared-ui/src/styles/buttons.css | 19 ++++++++++++++ .../shared-ui/src/styles/user-details.css | 7 +++++ packages/shared-ui/src/styles/variables.css | 2 +- 8 files changed, 98 insertions(+), 15 deletions(-) diff --git a/packages/react/src/components/ui/buttons/Button.tsx b/packages/react/src/components/ui/buttons/Button.tsx index 2a8338b6b..8819d52be 100644 --- a/packages/react/src/components/ui/buttons/Button.tsx +++ b/packages/react/src/components/ui/buttons/Button.tsx @@ -16,7 +16,12 @@ export const Button = forwardRef( ref={ref} {...rest} > - {isLoading ? : children} + {children} + {isLoading && ( +
+ +
+ )} ); }, diff --git a/packages/react/src/screens/user-details/EmailsEdit.tsx b/packages/react/src/screens/user-details/EmailsEdit.tsx index be88d0e23..3107c97f2 100644 --- a/packages/react/src/screens/user-details/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details/EmailsEdit.tsx @@ -22,6 +22,7 @@ const EmailsEdit = () => { const initialEmails = useRef(); + const [loading, setLoading] = useState(false); const [verifyingEmails, setVerifyingEmails] = useState([]); const [addingEmail, setAddingEmail] = useState(false); const [deletingEmail, setDeletingEmail] = useState(); @@ -60,10 +61,15 @@ const EmailsEdit = () => { }, [emails]); const addEmail = async () => { + if (loading) return; + if (!newEmail || !validateEmail(newEmail)) { setErrorMessage(warningEmail); return; } + + setLoading(true); + const res = await createIdentifier(LoginIdentifierType.Email, newEmail); if (res.err) { const code = getErrorCode(res.val.message); @@ -73,11 +79,16 @@ const EmailsEdit = () => { } console.error(t(`errors.${code}`)); } + setLoading(false); return; } - setNewEmail(''); - setAddingEmail(false); - void getCurrentUser(); + + void getCurrentUser() + .then(() => { + setNewEmail(''); + setAddingEmail(false); + }) + .finally(() => setLoading(false)); }; const startEmailVerification = (email: Identifier) => { @@ -181,6 +192,8 @@ const EmailsEdit = () => { diff --git a/packages/react/src/screens/user-details/PhonesEdit.tsx b/packages/react/src/screens/user-details/PhonesEdit.tsx index aca2261b8..b73cf0229 100644 --- a/packages/react/src/screens/user-details/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details/PhonesEdit.tsx @@ -22,6 +22,7 @@ const PhonesEdit = () => { const initialPhones = useRef(); + const [loading, setLoading] = useState(false); const [verifyingPhones, setVerifyingPhones] = useState([]); const [errorMessage, setErrorMessage] = useState(); const [deletingPhone, setDeletingPhone] = useState(); @@ -58,11 +59,13 @@ const PhonesEdit = () => { }, [phones]); const addPhone = async () => { + if (loading) return; + if (!newPhone) { setErrorMessage(t('user-details.warning_invalid_phone')); return; } - + setLoading(true); const res = await createIdentifier(LoginIdentifierType.Phone, newPhone); if (res.err) { const code = getErrorCode(res.val.message); @@ -73,12 +76,16 @@ const PhonesEdit = () => { console.error(t(`errors.${code}`)); } + setLoading(false); return; } - setNewPhone(''); - setAddingPhone(false); - void getCurrentUser(); + void getCurrentUser() + .then(() => { + setNewPhone(''); + setAddingPhone(false); + }) + .finally(() => setLoading(false)); }; const startPhoneVerification = (phone: Identifier) => { @@ -179,7 +186,9 @@ const PhonesEdit = () => { />
diff --git a/packages/shared-ui/src/styles/buttons.css b/packages/shared-ui/src/styles/buttons.css index 0f921ea9a..9647ec1f6 100644 --- a/packages/shared-ui/src/styles/buttons.css +++ b/packages/shared-ui/src/styles/buttons.css @@ -1,3 +1,7 @@ +button { + position: relative; +} + .cb-link { font-weight: var(--cb-font-weight-bold); text-decoration: none; @@ -117,3 +121,18 @@ button.cb-primary-button-error-variant { height: 1.5rem; vertical-align: middle; } + +.cb-button-loading { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background-color: inherit; + border: inherit; + border-radius: inherit; + color: inherit; +} diff --git a/packages/shared-ui/src/styles/user-details.css b/packages/shared-ui/src/styles/user-details.css index dbd5c7fad..b379b4c70 100644 --- a/packages/shared-ui/src/styles/user-details.css +++ b/packages/shared-ui/src/styles/user-details.css @@ -428,6 +428,13 @@ button.cb-user-details-deletion-dialog-secondary-button:hover { padding: 0.2rem 0.4rem; } +.cb-user-details-button-spinner { + border-width: 2px; + border-color: transparent var(--cb-white) var(--cb-white) var(--cb-white); + width: 8px; + height: 8px; +} + @media screen and (max-width: 481px) { .cb-user-details-section-indentifier { width: 75%; diff --git a/packages/shared-ui/src/styles/variables.css b/packages/shared-ui/src/styles/variables.css index 67365a3d2..29968e929 100644 --- a/packages/shared-ui/src/styles/variables.css +++ b/packages/shared-ui/src/styles/variables.css @@ -1,6 +1,6 @@ :root { --cb-primary-color: #1953ff; - --cb-primary-color-hover: rgba(25, 83, 255, 0.75); + --cb-primary-color-hover: #2b5dff; --cb-primary-color-disabled: #dfdfe0; --cb-button-text-primary-color: #fff; --cb-text-primary-color: #000000; From 56a575ad8dbc0769ef0f7f5413d0212f7c8f7baa Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Thu, 22 Aug 2024 17:19:05 +0200 Subject: [PATCH 49/55] fixes lint issues --- packages/react/src/screens/user-details/EmailsEdit.tsx | 4 +++- packages/react/src/screens/user-details/NameEdit.tsx | 4 +++- packages/react/src/screens/user-details/PhonesEdit.tsx | 4 +++- packages/react/src/screens/user-details/UsernameEdit.tsx | 8 ++++++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/react/src/screens/user-details/EmailsEdit.tsx b/packages/react/src/screens/user-details/EmailsEdit.tsx index 3107c97f2..3d1a5ad41 100644 --- a/packages/react/src/screens/user-details/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details/EmailsEdit.tsx @@ -61,7 +61,9 @@ const EmailsEdit = () => { }, [emails]); const addEmail = async () => { - if (loading) return; + if (loading) { + return; + } if (!newEmail || !validateEmail(newEmail)) { setErrorMessage(warningEmail); diff --git a/packages/react/src/screens/user-details/NameEdit.tsx b/packages/react/src/screens/user-details/NameEdit.tsx index eb78c7f7a..c5f1cf582 100644 --- a/packages/react/src/screens/user-details/NameEdit.tsx +++ b/packages/react/src/screens/user-details/NameEdit.tsx @@ -30,7 +30,9 @@ const NameEdit: FC = () => { const changeName = async () => { setErrorMessage(undefined); - if (loading) return; + if (loading) { + return; + } if (!name) { setErrorMessage(t('user-details.name_required')); diff --git a/packages/react/src/screens/user-details/PhonesEdit.tsx b/packages/react/src/screens/user-details/PhonesEdit.tsx index b73cf0229..8b486dcf8 100644 --- a/packages/react/src/screens/user-details/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details/PhonesEdit.tsx @@ -59,7 +59,9 @@ const PhonesEdit = () => { }, [phones]); const addPhone = async () => { - if (loading) return; + if (loading) { + return; + } if (!newPhone) { setErrorMessage(t('user-details.warning_invalid_phone')); diff --git a/packages/react/src/screens/user-details/UsernameEdit.tsx b/packages/react/src/screens/user-details/UsernameEdit.tsx index 4a923753c..f8f4cb36d 100644 --- a/packages/react/src/screens/user-details/UsernameEdit.tsx +++ b/packages/react/src/screens/user-details/UsernameEdit.tsx @@ -37,7 +37,9 @@ const UsernameEdit = () => { const addUsername = async () => { setErrorMessage(undefined); - if (loading) return; + if (loading) { + return; + } if (!username || !username.value) { setErrorMessage(t('user-details.username_required')); @@ -66,7 +68,9 @@ const UsernameEdit = () => { const changeUsername = async () => { setErrorMessage(undefined); - if (loading) return; + if (loading) { + return; + } setLoading(true); From 0af28af9e4033a78523e058bb6a8087c57aad8b7 Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Tue, 27 Aug 2024 09:52:45 +0200 Subject: [PATCH 50/55] adds fields validation --- packages/react/src/screens/user-details/EmailsEdit.tsx | 5 +++++ packages/react/src/screens/user-details/NameEdit.tsx | 8 +++++++- packages/react/src/screens/user-details/PhonesEdit.tsx | 4 ++++ packages/react/src/screens/user-details/UsernameEdit.tsx | 4 ++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/react/src/screens/user-details/EmailsEdit.tsx b/packages/react/src/screens/user-details/EmailsEdit.tsx index 3d1a5ad41..a505bb054 100644 --- a/packages/react/src/screens/user-details/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details/EmailsEdit.tsx @@ -79,6 +79,11 @@ const EmailsEdit = () => { if (code === 'identifier_already_in_use') { setErrorMessage(t('user-details.email_unique')); } + + if (code === 'identifier_invalid_format') { + setErrorMessage(t('errors.identifier_invalid_format.email')); + } + console.error(t(`errors.${code}`)); } setLoading(false); diff --git a/packages/react/src/screens/user-details/NameEdit.tsx b/packages/react/src/screens/user-details/NameEdit.tsx index c5f1cf582..3f69b1d14 100644 --- a/packages/react/src/screens/user-details/NameEdit.tsx +++ b/packages/react/src/screens/user-details/NameEdit.tsx @@ -9,6 +9,7 @@ import { ChangeIcon } from '../../components/ui/icons/ChangeIcon'; import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; +import { getErrorCode } from '../../util'; const NameEdit: FC = () => { const { updateFullName } = useCorbado(); @@ -42,7 +43,12 @@ const NameEdit: FC = () => { setLoading(true); const res = await updateFullName(name); if (res.err) { - // no possible error code + const code = getErrorCode(res.val.message); + + if (code === 'missing_full_name') { + setErrorMessage(t('errors.missing_full_name')); + } + console.error(res.val.message); setLoading(false); return; diff --git a/packages/react/src/screens/user-details/PhonesEdit.tsx b/packages/react/src/screens/user-details/PhonesEdit.tsx index 8b486dcf8..f38d4848b 100644 --- a/packages/react/src/screens/user-details/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details/PhonesEdit.tsx @@ -76,6 +76,10 @@ const PhonesEdit = () => { setErrorMessage(t('user-details.phone_unique')); } + if (code === 'identifier_invalid_format') { + setErrorMessage(t('errors.identifier_invalid_format.phone')); + } + console.error(t(`errors.${code}`)); } setLoading(false); diff --git a/packages/react/src/screens/user-details/UsernameEdit.tsx b/packages/react/src/screens/user-details/UsernameEdit.tsx index f8f4cb36d..2dfb65959 100644 --- a/packages/react/src/screens/user-details/UsernameEdit.tsx +++ b/packages/react/src/screens/user-details/UsernameEdit.tsx @@ -92,6 +92,10 @@ const UsernameEdit = () => { setErrorMessage(t('user-details.username_unique')); } + if (code === 'identifier_invalid_format') { + setErrorMessage(t('errors.identifier_invalid_format.username')); + } + setLoading(false); console.error(res.val.message); return; From 10777730d9accd2d5d45cb056c9eadfe98c3b1ee Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Tue, 27 Aug 2024 10:13:35 +0200 Subject: [PATCH 51/55] adds validation --- packages/react/src/screens/user-details/EmailsEdit.tsx | 5 +++-- packages/react/src/screens/user-details/PhonesEdit.tsx | 1 + packages/react/src/screens/user-details/UsernameEdit.tsx | 9 ++++++++- packages/react/src/util.ts | 5 ----- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/react/src/screens/user-details/EmailsEdit.tsx b/packages/react/src/screens/user-details/EmailsEdit.tsx index a505bb054..4f308462d 100644 --- a/packages/react/src/screens/user-details/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details/EmailsEdit.tsx @@ -12,7 +12,7 @@ import DropdownMenu from '../../components/user-details/DropdownMenu'; import UserDetailsCard from '../../components/user-details/UserDetailsCard'; import { useCorbado } from '../../hooks/useCorbado'; import { useCorbadoUserDetails } from '../../hooks/useCorbadoUserDetails'; -import { getErrorCode, validateEmail } from '../../util'; +import { getErrorCode } from '../../util'; import IdentifierDeleteDialog from './IdentifierDeleteDialog'; import IdentifierVerifyDialog from './IdentifierVerifyDialog'; @@ -65,7 +65,7 @@ const EmailsEdit = () => { return; } - if (!newEmail || !validateEmail(newEmail)) { + if (!newEmail) { setErrorMessage(warningEmail); return; } @@ -94,6 +94,7 @@ const EmailsEdit = () => { .then(() => { setNewEmail(''); setAddingEmail(false); + setErrorMessage(undefined); }) .finally(() => setLoading(false)); }; diff --git a/packages/react/src/screens/user-details/PhonesEdit.tsx b/packages/react/src/screens/user-details/PhonesEdit.tsx index f38d4848b..f9d8edcea 100644 --- a/packages/react/src/screens/user-details/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details/PhonesEdit.tsx @@ -90,6 +90,7 @@ const PhonesEdit = () => { .then(() => { setNewPhone(''); setAddingPhone(false); + setErrorMessage(undefined); }) .finally(() => setLoading(false)); }; diff --git a/packages/react/src/screens/user-details/UsernameEdit.tsx b/packages/react/src/screens/user-details/UsernameEdit.tsx index 2dfb65959..19963bdd5 100644 --- a/packages/react/src/screens/user-details/UsernameEdit.tsx +++ b/packages/react/src/screens/user-details/UsernameEdit.tsx @@ -53,6 +53,10 @@ const UsernameEdit = () => { setErrorMessage(t('user-details.username_unique')); } + if (code === 'identifier_invalid_format') { + setErrorMessage(t('errors.identifier_invalid_format.username')); + } + console.error(t(`errors.${code}`)); } @@ -61,7 +65,10 @@ const UsernameEdit = () => { } void getCurrentUser() - .then(() => setAddingUsername(false)) + .then(() => { + setAddingUsername(false); + setErrorMessage(undefined); + }) .finally(() => setLoading(false)); }; diff --git a/packages/react/src/util.ts b/packages/react/src/util.ts index 5d37a2a65..cd0e82dff 100644 --- a/packages/react/src/util.ts +++ b/packages/react/src/util.ts @@ -3,8 +3,3 @@ export const getErrorCode = (message: string) => { const matches = regex.exec(message); return matches ? matches[1] : undefined; }; - -export function validateEmail(email: string): boolean { - const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; - return emailRegex.test(email); -} From 0fd5182e58792eb0ebfc5b29806f6814e367b5a2 Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Tue, 27 Aug 2024 10:21:17 +0200 Subject: [PATCH 52/55] fixes verification screen not closing --- .../react/src/screens/user-details/IdentifierVerifyDialog.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx b/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx index fbf8d0e07..7c6954724 100644 --- a/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx +++ b/packages/react/src/screens/user-details/IdentifierVerifyDialog.tsx @@ -83,7 +83,8 @@ const IdentifierVerifyDialog: FC = ({ identifier, onCancel }) => { } return; } - void getCurrentUser(); + + void getCurrentUser().then(() => onCancel()); }; function startTimer() { From 7fe2dec7ba23e7fd2c0894ecef344e136364ab0b Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Tue, 27 Aug 2024 21:08:10 +0200 Subject: [PATCH 53/55] mark primary emails with badges --- .../src/contexts/CorbadoSessionContext.tsx | 2 + .../src/contexts/CorbadoSessionProvider.tsx | 8 +++ .../src/screens/user-details/EmailsEdit.tsx | 58 +++++++++++++------ .../src/screens/user-details/UsernameEdit.tsx | 2 +- packages/shared-ui/src/i18n/en.json | 1 + packages/types/src/session.ts | 1 + packages/web-core/openapi/spec_v2.yaml | 12 +++- packages/web-core/src/api/v2/api.ts | 31 ++++++++-- .../web-core/src/services/SessionService.ts | 11 ++++ packages/web-js/src/core/Corbado.ts | 4 ++ 10 files changed, 103 insertions(+), 27 deletions(-) diff --git a/packages/react/src/contexts/CorbadoSessionContext.tsx b/packages/react/src/contexts/CorbadoSessionContext.tsx index f24163b59..c9daa7d26 100644 --- a/packages/react/src/contexts/CorbadoSessionContext.tsx +++ b/packages/react/src/contexts/CorbadoSessionContext.tsx @@ -25,6 +25,7 @@ export interface CorbadoSessionContextProps { deleteIdentifier: (identifierId: string) => Promise>; verifyIdentifierStart: (identifierId: string) => Promise>; verifyIdentifierFinish: (identifierId: string, code: string) => Promise>; + makePrimary: (identifierId: string, identifierType: LoginIdentifierType) => Promise>; deleteUser: () => Promise>; globalError: NonRecoverableError | undefined; } @@ -48,6 +49,7 @@ export const initialContext: CorbadoSessionContextProps = { deleteIdentifier: missingImplementation, verifyIdentifierStart: missingImplementation, verifyIdentifierFinish: missingImplementation, + makePrimary: missingImplementation, deleteUser: missingImplementation, }; diff --git a/packages/react/src/contexts/CorbadoSessionProvider.tsx b/packages/react/src/contexts/CorbadoSessionProvider.tsx index 7cbc33d2c..cc2cb3d5e 100644 --- a/packages/react/src/contexts/CorbadoSessionProvider.tsx +++ b/packages/react/src/contexts/CorbadoSessionProvider.tsx @@ -135,6 +135,13 @@ export const CorbadoSessionProvider: FC = ({ [corbadoApp], ); + const makePrimary = useCallback( + (identifierId: string, identifierType: LoginIdentifierType) => { + return corbadoApp.sessionService.makePrimary(identifierId, identifierType); + }, + [corbadoApp], + ); + const deleteUser = useCallback(() => { return corbadoApp.sessionService.deleteUser(); }, [corbadoApp]); @@ -156,6 +163,7 @@ export const CorbadoSessionProvider: FC = ({ deleteIdentifier, verifyIdentifierStart, verifyIdentifierFinish, + makePrimary, deleteUser, getPasskeys, deletePasskey, diff --git a/packages/react/src/screens/user-details/EmailsEdit.tsx b/packages/react/src/screens/user-details/EmailsEdit.tsx index 4f308462d..ea13097b9 100644 --- a/packages/react/src/screens/user-details/EmailsEdit.tsx +++ b/packages/react/src/screens/user-details/EmailsEdit.tsx @@ -17,7 +17,7 @@ import IdentifierDeleteDialog from './IdentifierDeleteDialog'; import IdentifierVerifyDialog from './IdentifierVerifyDialog'; const EmailsEdit = () => { - const { createIdentifier } = useCorbado(); + const { createIdentifier, makePrimary } = useCorbado(); const { emails = [], getCurrentUser, emailEnabled } = useCorbadoUserDetails(); const initialEmails = useRef(); @@ -41,6 +41,7 @@ const EmailsEdit = () => { const buttonCancel = useMemo(() => t('user-details.cancel'), [t]); const buttonCopy = useMemo(() => t('user-details.copy'), [t]); const buttonAddEmail = useMemo(() => t('user-details.add_email'), [t]); + const buttonPrimary = useMemo(() => t('user-details.make_primary'), [t]); const buttonVerify = useMemo(() => t('user-details.verify'), [t]); const buttonRemove = useMemo(() => t('user-details.remove'), [t]); @@ -99,6 +100,18 @@ const EmailsEdit = () => { .finally(() => setLoading(false)); }; + const makeEmailPrimary = async (email: Identifier) => { + setLoading(true); + + const res = await makePrimary(email.id, LoginIdentifierType.Email); + if (res.err) { + console.error(res.val.message); + } + + void getCurrentUser() + .then(() => setLoading(false)); + } + const startEmailVerification = (email: Identifier) => { setVerifyingEmails(prev => [...prev, email]); }; @@ -111,17 +124,20 @@ const EmailsEdit = () => { return null; } - const getBadge = (email: Identifier) => { - switch (email.status) { - case 'primary': - return { text: badgePrimary, icon: }; - - case 'verified': - return { text: badgeVerified, icon: }; - - default: - return { text: badgePending, icon: }; + const getBadges = (email: Identifier) => { + const badges = []; + + if (email.status === 'verified') { + badges.push({ text: badgeVerified, icon: }); + } else { + badges.push({ text: badgePending, icon: }); } + + if (email.primary) { + badges.push({ text: badgePrimary, icon: }); + } + + return badges }; const copyEmail = async (email: string) => { @@ -131,7 +147,11 @@ const EmailsEdit = () => { const getMenuItems = (email: Identifier) => { const items = [buttonCopy]; - if (email.status !== 'verified') { + if (email.status === 'verified') { + if (!email.primary) { + items.push(buttonPrimary); + } + } else { items.push(buttonVerify); } @@ -157,15 +177,19 @@ const EmailsEdit = () => {
{email.value} -
- {getBadge(email).icon} - {getBadge(email).text} -
+ {getBadges(email).map(badge => ( +
+ {badge.icon} + {badge.text} +
+ ))}
{ - if (item === buttonVerify) { + if (item === buttonPrimary) { + void makeEmailPrimary(email); + } else if (item === buttonVerify) { void startEmailVerification(email); } else if (item === buttonRemove) { setDeletingEmail(email); diff --git a/packages/react/src/screens/user-details/UsernameEdit.tsx b/packages/react/src/screens/user-details/UsernameEdit.tsx index 19963bdd5..5d590e9fc 100644 --- a/packages/react/src/screens/user-details/UsernameEdit.tsx +++ b/packages/react/src/screens/user-details/UsernameEdit.tsx @@ -133,7 +133,7 @@ const UsernameEdit = () => { className='cb-user-details-input' value={username?.value} errorMessage={errorMessage} - onChange={e => setUsername({ id: '', type: 'username', status: 'verified', value: e.target.value })} + onChange={e => setUsername({ id: '', type: 'username', status: 'verified', primary: false, value: e.target.value })} /> request(axios, basePath)); }, /** - * Updates current user + * Updates current user (full name, primary email, or primary phone) * @param {MeUpdateReq} meUpdateReq * @param {*} [options] Override http request option. * @throws {RequiredError} @@ -5695,7 +5714,7 @@ export class UsersApi extends BaseAPI { } /** - * Updates current user + * Updates current user (full name, primary email, or primary phone) * @param {MeUpdateReq} meUpdateReq * @param {*} [options] Override http request option. * @throws {RequiredError} diff --git a/packages/web-core/src/services/SessionService.ts b/packages/web-core/src/services/SessionService.ts index a8f8b082a..38621315d 100644 --- a/packages/web-core/src/services/SessionService.ts +++ b/packages/web-core/src/services/SessionService.ts @@ -225,6 +225,17 @@ export class SessionService { }); } + async makePrimary(identifierId: string, identifierType: LoginIdentifierType): Promise> { + return Result.wrapAsync(async () => { + if (identifierType === LoginIdentifierType.Email) { + await this.#usersApi.currentUserUpdate({ primaryEmailIdentifierID: identifierId }); + } else if (identifierType === LoginIdentifierType.Username) { + await this.#usersApi.currentUserUpdate({ primaryPhoneIdentifierID: identifierId }); + } + return void 0; + }); + } + async deleteUser(): Promise> { return Result.wrapAsync(async () => { await this.#usersApi.currentUserDelete({}); diff --git a/packages/web-js/src/core/Corbado.ts b/packages/web-js/src/core/Corbado.ts index 0ca3ad904..68a43d31d 100644 --- a/packages/web-js/src/core/Corbado.ts +++ b/packages/web-js/src/core/Corbado.ts @@ -136,6 +136,10 @@ export class Corbado { return this.#getCorbadoAppState().corbadoApp.sessionService.verifyIdentifierFinish(identifierId, code); } + makePrimary(identifierId: string, identifierType: LoginIdentifierType) { + return this.#getCorbadoAppState().corbadoApp.sessionService.makePrimary(identifierId, identifierType); + } + deleteUser() { return this.#getCorbadoAppState().corbadoApp.sessionService.deleteUser(); } From 08c7e30d385226ce7422090bb89d23b1703e002f Mon Sep 17 00:00:00 2001 From: Anders Choi Date: Tue, 27 Aug 2024 21:59:51 +0200 Subject: [PATCH 54/55] phone primary --- .../src/screens/user-details/PhonesEdit.tsx | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/packages/react/src/screens/user-details/PhonesEdit.tsx b/packages/react/src/screens/user-details/PhonesEdit.tsx index f9d8edcea..6844346e3 100644 --- a/packages/react/src/screens/user-details/PhonesEdit.tsx +++ b/packages/react/src/screens/user-details/PhonesEdit.tsx @@ -17,7 +17,7 @@ import IdentifierDeleteDialog from './IdentifierDeleteDialog'; import IdentifierVerifyDialog from './IdentifierVerifyDialog'; const PhonesEdit = () => { - const { createIdentifier } = useCorbado(); + const { createIdentifier, makePrimary } = useCorbado(); const { phones = [], getCurrentUser, phoneEnabled } = useCorbadoUserDetails(); const initialPhones = useRef(); @@ -39,6 +39,7 @@ const PhonesEdit = () => { const buttonCopy = useMemo(() => t('user-details.copy'), [t]); const buttonCancel = useMemo(() => t('user-details.cancel'), [t]); const buttonAddPhone = useMemo(() => t('user-details.add_phone'), [t]); + const buttonPrimary = useMemo(() => t('user-details.make_primary'), [t]); const buttonVerify = useMemo(() => t('user-details.verify'), [t]); const buttonRemove = useMemo(() => t('user-details.remove'), [t]); @@ -67,7 +68,9 @@ const PhonesEdit = () => { setErrorMessage(t('user-details.warning_invalid_phone')); return; } + setLoading(true); + const res = await createIdentifier(LoginIdentifierType.Phone, newPhone); if (res.err) { const code = getErrorCode(res.val.message); @@ -95,6 +98,18 @@ const PhonesEdit = () => { .finally(() => setLoading(false)); }; + const makePhonePrimary = async (phone: Identifier) => { + setLoading(true); + + const res = await makePrimary(phone.id, LoginIdentifierType.Phone); + if (res.err) { + console.error(res.val.message); + } + + void getCurrentUser() + .then(() => setLoading(false)); + } + const startPhoneVerification = (phone: Identifier) => { setVerifyingPhones(prev => [...prev, phone]); }; @@ -107,17 +122,20 @@ const PhonesEdit = () => { return null; } - const getBadge = (phone: Identifier) => { - switch (phone.status) { - case 'primary': - return { text: badgePrimary, icon: }; - - case 'verified': - return { text: badgeVerified, icon: }; - - default: - return { text: badgePending, icon: }; + const getBadges = (email: Identifier) => { + const badges = []; + + if (email.status === 'verified') { + badges.push({ text: badgeVerified, icon: }); + } else { + badges.push({ text: badgePending, icon: }); + } + + if (email.primary) { + badges.push({ text: badgePrimary, icon: }); } + + return badges }; const getMenuItems = (phone: Identifier) => { @@ -153,15 +171,19 @@ const PhonesEdit = () => {
{phone.value} -
- {getBadge(phone).icon} - {getBadge(phone).text} -
+ {getBadges(phone).map(badge => ( +
+ {badge.icon} + {badge.text} +
+ ))}
{ - if (item === buttonVerify) { + if (item === buttonPrimary) { + void makePhonePrimary(phone); + } else if (item === buttonVerify) { void startPhoneVerification(phone); } else if (item === buttonRemove) { setDeletingPhone(phone); From 299eb7289cf945272acc966a4c0f086135889b08 Mon Sep 17 00:00:00 2001 From: Dopeamin Date: Wed, 28 Aug 2024 10:13:15 +0200 Subject: [PATCH 55/55] fixes loading issue --- packages/react/src/screens/user-details/UsernameEdit.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/react/src/screens/user-details/UsernameEdit.tsx b/packages/react/src/screens/user-details/UsernameEdit.tsx index 5d590e9fc..9df2a30d4 100644 --- a/packages/react/src/screens/user-details/UsernameEdit.tsx +++ b/packages/react/src/screens/user-details/UsernameEdit.tsx @@ -79,8 +79,6 @@ const UsernameEdit = () => { return; } - setLoading(true); - if (!username || !newUsername) { setErrorMessage(t('user-details.username_required')); return; @@ -91,6 +89,8 @@ const UsernameEdit = () => { return; } + setLoading(true); + const res = await updateUsername(username.id, newUsername); if (res.err) { const code = getErrorCode(res.val.message); @@ -133,7 +133,9 @@ const UsernameEdit = () => { className='cb-user-details-input' value={username?.value} errorMessage={errorMessage} - onChange={e => setUsername({ id: '', type: 'username', status: 'verified', primary: false, value: e.target.value })} + onChange={e => + setUsername({ id: '', type: 'username', status: 'verified', primary: false, value: e.target.value }) + } />