From ba294164a46699f36b7fc7a08391912d797ace14 Mon Sep 17 00:00:00 2001
From: Kiril Kartunov
Date: Mon, 5 Jun 2023 11:13:07 +0300
Subject: [PATCH 1/3] MP-40 account security init
---
.../accounts/src/lib/assets/security/index.ts | 6 +
.../src/settings/tabs/account/AccountTab.tsx | 2 +-
.../tabs/account/security/Security.tsx | 42 ++-
.../DiceSetupModal.module.scss | 30 ++
.../dice-setup-modal/DiceSetupModal.tsx | 268 +++++++++++++++++-
.../VerificationListener/index.tsx | 29 ++
.../account/user-and-pass/UserAndPassword.tsx | 3 +
src/config/environments/default.env.ts | 6 +
.../environments/global-config.model.ts | 1 +
.../core/lib/profile/data-providers/index.ts | 2 +
.../data-providers/useDiceIdConnection.ts | 23 ++
.../data-providers/useMemberMFAStatus.ts | 20 ++
src/libs/core/lib/profile/index.ts | 1 +
.../core/lib/profile/modify-user-mfa.model.ts | 14 +
.../lib/profile/profile-functions/index.ts | 1 +
.../profile-functions/profile-store/index.ts | 2 +
.../profile-store/profile-endpoint.config.ts | 8 +
.../profile-store/profile-xhr.store.ts | 11 +-
.../profile-functions/profile.functions.ts | 9 +
.../FormToggleSwitch.module.scss | 2 +-
20 files changed, 460 insertions(+), 20 deletions(-)
create mode 100644 src/apps/accounts/src/settings/tabs/account/security/dice-setup-modal/DiceSetupModal.module.scss
create mode 100644 src/apps/accounts/src/settings/tabs/account/security/dice-setup-modal/VerificationListener/index.tsx
create mode 100644 src/libs/core/lib/profile/data-providers/useDiceIdConnection.ts
create mode 100644 src/libs/core/lib/profile/data-providers/useMemberMFAStatus.ts
create mode 100644 src/libs/core/lib/profile/modify-user-mfa.model.ts
diff --git a/src/apps/accounts/src/lib/assets/security/index.ts b/src/apps/accounts/src/lib/assets/security/index.ts
index 6b2d378b3..80b9111c0 100644
--- a/src/apps/accounts/src/lib/assets/security/index.ts
+++ b/src/apps/accounts/src/lib/assets/security/index.ts
@@ -1,7 +1,13 @@
import { ReactComponent as MFAImage } from './mfa.svg'
+import { ReactComponent as AppleStore } from './apple-store.svg'
import diceIdLogo from './dicelogo.png'
+import diceIdLogoBig from './dicelogobig.png'
+import googlePlay from './google-play.png'
export {
+ AppleStore,
diceIdLogo,
+ diceIdLogoBig,
+ googlePlay,
MFAImage,
}
diff --git a/src/apps/accounts/src/settings/tabs/account/AccountTab.tsx b/src/apps/accounts/src/settings/tabs/account/AccountTab.tsx
index f6ceadfc0..3439dbc69 100644
--- a/src/apps/accounts/src/settings/tabs/account/AccountTab.tsx
+++ b/src/apps/accounts/src/settings/tabs/account/AccountTab.tsx
@@ -19,7 +19,7 @@ const AccountTab: FC = (props: AccountTabProps) => (
-
+
)
diff --git a/src/apps/accounts/src/settings/tabs/account/security/Security.tsx b/src/apps/accounts/src/settings/tabs/account/security/Security.tsx
index 25bca2dbc..6c05311df 100644
--- a/src/apps/accounts/src/settings/tabs/account/security/Security.tsx
+++ b/src/apps/accounts/src/settings/tabs/account/security/Security.tsx
@@ -1,18 +1,43 @@
-import { Dispatch, FC, SetStateAction, useState } from 'react'
+import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'
+import { toast } from 'react-toastify'
import { Button, Collapsible, FormToggleSwitch } from '~/libs/ui'
import { diceIdLogo, MFAImage, SettingSection } from '~/apps/accounts/src/lib'
+import { MemberMFAStatus, updateMemberMFAStatusAsync, useMemberMFAStatus, UserProfile } from '~/libs/core'
import { DiceSetupModal } from './dice-setup-modal'
import styles from './Security.module.scss'
-const Security: FC<{}> = () => {
- const [mfaStatus, setMFAStatus]: [boolean, Dispatch>] = useState(false)
+interface SecurityProps {
+ profile: UserProfile
+}
+
+const Security: FC = (props: SecurityProps) => {
const [setupDiceModalOpen, setSetupDiceModalOpen]: [boolean, Dispatch>] = useState(false)
- function handleUserMFAChange(event: any): void {
- console.log('handleUserMFAChange', event)
- setMFAStatus(!mfaStatus)
+ const mfaStatusData: MemberMFAStatus | undefined = useMemberMFAStatus(props.profile.userId)
+
+ const [mfaEnabled, setMFAEnabled]: [boolean, Dispatch>] = useState(false)
+
+ useEffect(() => {
+ if (mfaStatusData) {
+ setMFAEnabled(mfaStatusData.mfaEnabled)
+ }
+ }, [mfaStatusData])
+
+ function handleUserMFAChange(): void {
+ updateMemberMFAStatusAsync(props.profile.userId, {
+ param: {
+ mfaEnabled: !mfaEnabled,
+ },
+ })
+ .then(() => {
+ setMFAEnabled(!mfaEnabled)
+ toast.success('Your Multi Factor Authentication (MFA) status was updated.')
+ })
+ .catch(() => {
+ toast.error('Something went wrong. Please try again later.')
+ })
}
function handleDiceModalStatus(): void {
@@ -38,7 +63,8 @@ const Security: FC<{}> = () => {
)}
/>
@@ -58,6 +84,7 @@ const Security: FC<{}> = () => {
size='lg'
className={styles.diceIdButton}
onClick={handleDiceModalStatus}
+ disabled={!mfaEnabled || mfaStatusData?.diceEnabled}
/>
)}
/>
@@ -65,6 +92,7 @@ const Security: FC<{}> = () => {
{setupDiceModalOpen && (
)}
diff --git a/src/apps/accounts/src/settings/tabs/account/security/dice-setup-modal/DiceSetupModal.module.scss b/src/apps/accounts/src/settings/tabs/account/security/dice-setup-modal/DiceSetupModal.module.scss
new file mode 100644
index 000000000..5855168b2
--- /dev/null
+++ b/src/apps/accounts/src/settings/tabs/account/security/dice-setup-modal/DiceSetupModal.module.scss
@@ -0,0 +1,30 @@
+@import '@libs/ui/styles/includes';
+
+.diceModal {
+ :global(.react-responsive-modal-closeButton) {
+ display: flex;
+ }
+
+ .appSoresWrap {
+ display: flex;
+ justify-content: space-evenly;
+ margin: $sp-4 0;
+
+ .appStoreCard {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+ }
+
+ .qrCode {
+ margin: auto;
+ }
+
+ .ctaButtons {
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ flex: 1;
+ }
+}
\ No newline at end of file
diff --git a/src/apps/accounts/src/settings/tabs/account/security/dice-setup-modal/DiceSetupModal.tsx b/src/apps/accounts/src/settings/tabs/account/security/dice-setup-modal/DiceSetupModal.tsx
index bf3620c68..46e5e940c 100644
--- a/src/apps/accounts/src/settings/tabs/account/security/dice-setup-modal/DiceSetupModal.tsx
+++ b/src/apps/accounts/src/settings/tabs/account/security/dice-setup-modal/DiceSetupModal.tsx
@@ -1,18 +1,266 @@
-import { FC } from 'react'
+import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'
+import { QRCodeSVG } from 'qrcode.react'
+import { get, isUndefined, lowerCase } from 'lodash'
+import { toast } from 'react-toastify'
-import { BaseModal } from '~/libs/ui'
+import { BaseModal, Button } from '~/libs/ui'
+import { AppleStore, diceIdLogoBig, googlePlay } from '~/apps/accounts/src/lib'
+import { DiceConnectionStatus, updateMemberMFAStatusAsync, useDiceIdConnection, UserProfile } from '~/libs/core'
+import { EnvironmentConfig } from '~/config'
+import { VerificationListener } from './VerificationListener'
+import styles from './DiceSetupModal.module.scss'
+
+const GooglePlayLink: string = 'https://play.google.com/store/apps/details?id=com.diwallet1'
+const AppleStoreLink: string = 'https://apps.apple.com/us/app/dice-id/id1548148979'
interface DiceSetupModalProps {
onClose: () => void
+ profile: UserProfile
}
-const DiceSetupModal: FC = (props: DiceSetupModalProps) => (
-
-)
+const DiceSetupModal: FC = (props: DiceSetupModalProps) => {
+ const [step, setStep]: [number, Dispatch>] = useState(1)
+
+ const [diceConnectionId, setDiceConnectionId]: [
+ number | undefined,
+ Dispatch>
+ ] = useState()
+
+ const diceConnection: DiceConnectionStatus | undefined = useDiceIdConnection(props.profile.userId, diceConnectionId)
+
+ const [isVerificationProcessing, setIsVerificationProcessing]: [boolean, Dispatch>]
+ = useState(false)
+
+ useEffect(() => {
+ if (diceConnection && !diceConnectionId) {
+ setDiceConnectionId(diceConnection.id)
+ }
+ }, [
+ diceConnection,
+ diceConnectionId,
+ ])
+
+ function handleSecondaryButtonClick(): void {
+ if (step === 1 || step === 4) {
+ props.onClose()
+ } else {
+ setStep(step - 1)
+ }
+ }
+
+ function handlePrimaryButtonClick(): void {
+ if (step >= 3) {
+ props.onClose()
+ } else {
+ setStep(step + 1)
+ }
+ }
+
+ function verificationCallback(data: any): void {
+ if (data.success) {
+ const userEmail: string = get(data, 'user.profile.Email')
+ if (!isUndefined(userEmail) && lowerCase(userEmail) === lowerCase(props.profile.email)) {
+ updateMemberMFAStatusAsync(props.profile.userId, {
+ param: {
+ diceEnabled: true,
+ },
+ })
+ .then(() => {
+ setStep(4)
+ // eslint-disable-next-line max-len
+ toast.success('Your credentials have been verified and you are all set for MFA using your decentralized identity (DICE ID).')
+ })
+ .catch(() => {
+ toast.error('Something went wrong. Please try again later.')
+ })
+ } else {
+ setStep(4)
+ }
+ } else {
+ setStep(5)
+ }
+ }
+
+ function onStartProcessing(): void {
+ setIsVerificationProcessing(true)
+ }
+
+ return (
+
+
+ {
+ step <= 4 && (
+
+ )
+ }
+
+ )}
+ >
+ {
+ step < 4 && (
+
+ Step
+ {' '}
+ {step}
+ {' '}
+ of 3
+
+ )
+ }
+ {
+ step === 1 && (
+ <>
+ First, please download the DICE ID App from the Google Play Store or the iOS App Store.
+
+
+ After you have downloaded and installed the mobile app,
+ make sure to complete the configuration process.
+ When ready, click next below.
+
+ >
+ )
+ }
+ {
+ step === 2 && (
+ <>
+ Scan the following DICE ID QR Code in your DICE ID mobile application.
+ {
+ diceConnection ? (
+
+ ) : (
+ Loading...
+ )
+ }
+
+ Once the connection is established, the service will offer you
+ a Verifiable Credential.
+
+ Press the ACCEPT button in your DICE ID App.
+
+ If you DECLINE the invitation, please try again after 5 minutes.
+
+ >
+ )
+ }
+ {
+ step === 3 && (
+ <>
+
+ Scan the following DICE ID QR Code in your DICE ID
+ mobile application to confirm your credential.
+
+
+ {isVerificationProcessing && (
+
+ Powered by DICE ID
+
+ )}
+
+ >
+ )
+ }
+ {
+ step === 4 && (
+ <>
+ Setup completed!
+
+ Hello
+ {' '}
+ {props.profile.handle}
+ ,
+
+
+ Your credentials have been verified and you are all set
+ for MFA using your decentralized identity (DICE ID).
+
+
+
+ For more information on DICE ID, please visit
+
+ https://www.diceid.com
+
+
+ Please click Finish bellow.
+ >
+ )
+ }
+ {
+ step === 5 && (
+ <>
+
+ >
+ )
+ }
+
+ )
+}
export default DiceSetupModal
diff --git a/src/apps/accounts/src/settings/tabs/account/security/dice-setup-modal/VerificationListener/index.tsx b/src/apps/accounts/src/settings/tabs/account/security/dice-setup-modal/VerificationListener/index.tsx
new file mode 100644
index 000000000..c884852d7
--- /dev/null
+++ b/src/apps/accounts/src/settings/tabs/account/security/dice-setup-modal/VerificationListener/index.tsx
@@ -0,0 +1,29 @@
+import { FC, useCallback, useEffect } from 'react'
+
+interface VerificationListenerProps {
+ event: string
+ callback: (data: object) => void
+ origin: string
+ type: string
+ onProcessing: () => void
+ startType: string
+}
+
+export const VerificationListener: FC = (props: VerificationListenerProps) => {
+ const messageHandler: (e: any) => void = useCallback(e => {
+ if (e.origin === props.origin && e.data && e.data.type) {
+ if (e.data.type === props.startType) {
+ props.onProcessing()
+ } else if (e.data.type === props.type) {
+ props.callback(e.data)
+ }
+ }
+ }, [props])
+
+ useEffect(() => {
+ window.addEventListener(props.event, messageHandler)
+ return () => window.removeEventListener(props.event, messageHandler)
+ }, [props.event, messageHandler])
+
+ return <>>
+}
diff --git a/src/apps/accounts/src/settings/tabs/account/user-and-pass/UserAndPassword.tsx b/src/apps/accounts/src/settings/tabs/account/user-and-pass/UserAndPassword.tsx
index 32c602b90..d1eec06c2 100644
--- a/src/apps/accounts/src/settings/tabs/account/user-and-pass/UserAndPassword.tsx
+++ b/src/apps/accounts/src/settings/tabs/account/user-and-pass/UserAndPassword.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable no-console */
import { Dispatch, FC, useCallback, useState } from 'react'
import { Collapsible, Form, FormInputModel, FormToggleSwitch } from '~/libs/ui'
@@ -36,6 +37,8 @@ const UserAndPassword: FC = (props: UserAndPasswordProps)
function shouldDisableChangePasswordButton(): boolean {
// pass reset form validation
+ console.log('formValues', formValues)
+
return true
}
diff --git a/src/config/environments/default.env.ts b/src/config/environments/default.env.ts
index 58cc145c3..fe2b766f6 100644
--- a/src/config/environments/default.env.ts
+++ b/src/config/environments/default.env.ts
@@ -63,3 +63,9 @@ export const PRIVACY_POLICY_URL = `${TOPCODER_URL}/policy`
export const SUBDOMAIN = window.location.hostname.split('.')[0]
export const GAMIFICATION_ORG_ID = getReactEnv('GAMIFICATION_ORG_ID', undefined)
+
+export const DICE_VERIFY_URL = get({
+ dev: 'https://accounts-auth0.topcoder-dev.com',
+ prod: 'https://accounts-auth0.topcoder.com',
+ qa: 'https://accounts-auth0.topcoder-qa.com',
+}, ENV, 'https://accounts-auth0.topcoder.com')
diff --git a/src/config/environments/global-config.model.ts b/src/config/environments/global-config.model.ts
index b5513094f..ed3e54347 100644
--- a/src/config/environments/global-config.model.ts
+++ b/src/config/environments/global-config.model.ts
@@ -39,4 +39,5 @@ export interface GlobalConfig {
},
SUBDOMAIN: string,
GAMIFICATION_ORG_ID: string
+ DICE_VERIFY_URL: string
}
diff --git a/src/libs/core/lib/profile/data-providers/index.ts b/src/libs/core/lib/profile/data-providers/index.ts
index 864d8e6a0..861433b32 100644
--- a/src/libs/core/lib/profile/data-providers/index.ts
+++ b/src/libs/core/lib/profile/data-providers/index.ts
@@ -6,3 +6,5 @@ export * from './useUserCompletedCertifications'
export * from './useStatsHistory'
export * from './useStatsDistribution'
export * from './useMemberEmailPreferences'
+export * from './useMemberMFAStatus'
+export * from './useDiceIdConnection'
diff --git a/src/libs/core/lib/profile/data-providers/useDiceIdConnection.ts b/src/libs/core/lib/profile/data-providers/useDiceIdConnection.ts
new file mode 100644
index 000000000..484bfa1b2
--- /dev/null
+++ b/src/libs/core/lib/profile/data-providers/useDiceIdConnection.ts
@@ -0,0 +1,23 @@
+import useSWR, { SWRConfiguration, SWRResponse } from 'swr'
+
+import { diceIDURL } from '~/libs/core'
+
+export interface DiceConnectionStatus {
+ accepted: boolean
+ connection: string
+ createdAt: Date
+ id: number
+}
+
+export function useDiceIdConnection(userId: number, connectionId?: number): DiceConnectionStatus | undefined {
+ const options: SWRConfiguration = {}
+
+ if (connectionId) {
+ options.refreshInterval = 5000 // pooling interval 5s if connection is active
+ }
+
+ const { data }: SWRResponse
+ = useSWR(`${diceIDURL(userId)}/diceConnection${connectionId ? `/${connectionId}` : ''}`, options)
+
+ return data ? data.result.content : undefined
+}
diff --git a/src/libs/core/lib/profile/data-providers/useMemberMFAStatus.ts b/src/libs/core/lib/profile/data-providers/useMemberMFAStatus.ts
new file mode 100644
index 000000000..0a3b6dc7f
--- /dev/null
+++ b/src/libs/core/lib/profile/data-providers/useMemberMFAStatus.ts
@@ -0,0 +1,20 @@
+import useSWR, { SWRResponse } from 'swr'
+
+import { memberModifyMfaURL } from '~/libs/core'
+
+export interface MemberMFAStatus {
+ createdAt: Date
+ createdBy: number
+ diceEnabled: boolean
+ id: number
+ mfaEnabled: boolean
+ modifiedAt: Date
+ modifiedBy: number
+ userId: number
+}
+
+export function useMemberMFAStatus(userId: number): MemberMFAStatus | undefined {
+ const { data }: SWRResponse = useSWR(memberModifyMfaURL(userId))
+
+ return data ? data.result.content : undefined
+}
diff --git a/src/libs/core/lib/profile/index.ts b/src/libs/core/lib/profile/index.ts
index c7bec0499..12cc8c5ed 100644
--- a/src/libs/core/lib/profile/index.ts
+++ b/src/libs/core/lib/profile/index.ts
@@ -11,3 +11,4 @@ export * from './user-badge.model'
export * from './modify-user-role.model'
export * from './user-email-preference.model'
export * from './modify-user-email-preferences.model'
+export * from './modify-user-mfa.model'
diff --git a/src/libs/core/lib/profile/modify-user-mfa.model.ts b/src/libs/core/lib/profile/modify-user-mfa.model.ts
new file mode 100644
index 000000000..a21bcdf40
--- /dev/null
+++ b/src/libs/core/lib/profile/modify-user-mfa.model.ts
@@ -0,0 +1,14 @@
+export interface ModifyUserMFARequest {
+ param: {
+ mfaEnabled?: boolean
+ diceEnabled?: boolean
+ }
+}
+
+export interface ModifyUserMFAResponse {
+ id: string
+ result: {
+ content: string
+ success: boolean
+ }
+}
diff --git a/src/libs/core/lib/profile/profile-functions/index.ts b/src/libs/core/lib/profile/profile-functions/index.ts
index 85d3bc90b..e161c476b 100644
--- a/src/libs/core/lib/profile/profile-functions/index.ts
+++ b/src/libs/core/lib/profile/profile-functions/index.ts
@@ -8,6 +8,7 @@ export {
editNameAsync as profileEditNameAsync,
updatePrimaryMemberRoleAsync,
updateMemberEmailPreferencesAsync,
+ updateMemberMFAStatusAsync,
} from './profile.functions'
export * from './profile-store'
export * from './rating.functions'
diff --git a/src/libs/core/lib/profile/profile-functions/profile-store/index.ts b/src/libs/core/lib/profile/profile-functions/profile-store/index.ts
index e799ee1e2..7f904b77b 100644
--- a/src/libs/core/lib/profile/profile-functions/profile-store/index.ts
+++ b/src/libs/core/lib/profile/profile-functions/profile-store/index.ts
@@ -12,4 +12,6 @@ export {
learnBaseURL,
memberStatsDistroURL,
memberEmailPreferencesURL,
+ memberModifyMfaURL,
+ diceIDURL,
} from './profile-endpoint.config'
diff --git a/src/libs/core/lib/profile/profile-functions/profile-store/profile-endpoint.config.ts b/src/libs/core/lib/profile/profile-functions/profile-store/profile-endpoint.config.ts
index fff2789e5..20956b134 100644
--- a/src/libs/core/lib/profile/profile-functions/profile-store/profile-endpoint.config.ts
+++ b/src/libs/core/lib/profile/profile-functions/profile-store/profile-endpoint.config.ts
@@ -35,3 +35,11 @@ export function memberEmailPreferencesURL(): string {
// Also, note the audience id is hardcoded here NO DEV audience exists in Mailchimp
return `https://community-app.${EnvironmentConfig.TC_DOMAIN}/api/mailchimp/28bfd3c062/members`
}
+
+export function memberModifyMfaURL(userId: number): string {
+ return `${EnvironmentConfig.API.V3}/users/${userId}/2fa`
+}
+
+export function diceIDURL(userId: number): string {
+ return `${EnvironmentConfig.API.V3}/users/${userId}`
+}
diff --git a/src/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store.ts b/src/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store.ts
index e4d865300..fc5765af7 100644
--- a/src/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store.ts
+++ b/src/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store.ts
@@ -1,7 +1,8 @@
-import { xhrGetAsync, xhrPostAsync, xhrPutAsync } from '../../../xhr'
+import { xhrGetAsync, xhrPatchAsync, xhrPostAsync, xhrPutAsync } from '../../../xhr'
import { CountryLookup } from '../../country-lookup.model'
import { EditNameRequest } from '../../edit-name-request.model'
import { ModifyMemberEmailPreferencesRequest } from '../../modify-user-email-preferences.model'
+import { ModifyUserMFARequest, ModifyUserMFAResponse } from '../../modify-user-mfa.model'
import { ModifyUserRoleRequest, ModifyUserRoleResponse } from '../../modify-user-role.model'
import { UserEmailPreferences } from '../../user-email-preference.model'
import { UserProfile } from '../../user-profile.model'
@@ -11,6 +12,7 @@ import { UserVerify } from '../../user-verify.model'
import {
countryLookupURL,
memberEmailPreferencesURL,
+ memberModifyMfaURL,
memberModifyRoleURL,
profile as profileUrl,
verify as verifyUrl,
@@ -57,3 +59,10 @@ export async function updateMemberEmailPreferences(
request,
)
}
+
+export async function updateMemberMFA(userId: number, payload: ModifyUserMFARequest): Promise {
+ return xhrPatchAsync(
+ memberModifyMfaURL(userId),
+ payload,
+ )
+}
diff --git a/src/libs/core/lib/profile/profile-functions/profile.functions.ts b/src/libs/core/lib/profile/profile-functions/profile.functions.ts
index 3ad171144..fc079534c 100644
--- a/src/libs/core/lib/profile/profile-functions/profile.functions.ts
+++ b/src/libs/core/lib/profile/profile-functions/profile.functions.ts
@@ -2,6 +2,7 @@ import { tokenGetAsync, TokenModel, userGetDiceStatusAsync } from '../../auth'
import { CountryLookup } from '../country-lookup.model'
import { EditNameRequest } from '../edit-name-request.model'
import { ModifyMemberEmailPreferencesRequest } from '../modify-user-email-preferences.model'
+import { ModifyUserMFARequest, ModifyUserMFAResponse } from '../modify-user-mfa.model'
import { ModifyUserRoleResponse } from '../modify-user-role.model'
import { UserEmailPreferences } from '../user-email-preference.model'
import { UserProfile } from '../user-profile.model'
@@ -13,6 +14,7 @@ import { getMemberStats, getVerification, profileStoreGet, profileStorePatchName
import {
getCountryLookup,
updateMemberEmailPreferences,
+ updateMemberMFA,
updatePrimaryMemberRole,
} from './profile-store/profile-xhr.store'
@@ -81,3 +83,10 @@ export async function updateMemberEmailPreferencesAsync(
): Promise {
return updateMemberEmailPreferences(email, emailPreferences)
}
+
+export async function updateMemberMFAStatusAsync(
+ userId: number,
+ payload: ModifyUserMFARequest,
+): Promise {
+ return updateMemberMFA(userId, payload)
+}
diff --git a/src/libs/ui/lib/components/form/form-groups/form-toggle-switch/FormToggleSwitch.module.scss b/src/libs/ui/lib/components/form/form-groups/form-toggle-switch/FormToggleSwitch.module.scss
index 80c8f4998..4128fc515 100644
--- a/src/libs/ui/lib/components/form/form-groups/form-toggle-switch/FormToggleSwitch.module.scss
+++ b/src/libs/ui/lib/components/form/form-groups/form-toggle-switch/FormToggleSwitch.module.scss
@@ -80,7 +80,7 @@
&.disabled {
.label {
- background-color: $black-20;
+ background-color: $black-20 !important;
}
}
}
From e2412ebd11d08719832a7651a07b872ad7dc20d0 Mon Sep 17 00:00:00 2001
From: Kiril Kartunov
Date: Mon, 5 Jun 2023 11:57:27 +0300
Subject: [PATCH 2/3] MP-43 add reset selection button
---
.../tabs/payments/PaymentsTab.module.scss | 16 +++++++
.../settings/tabs/payments/PaymentsTab.tsx | 44 ++++++++++++++++---
2 files changed, 55 insertions(+), 5 deletions(-)
diff --git a/src/apps/accounts/src/settings/tabs/payments/PaymentsTab.module.scss b/src/apps/accounts/src/settings/tabs/payments/PaymentsTab.module.scss
index 02b570338..0c2e430f5 100644
--- a/src/apps/accounts/src/settings/tabs/payments/PaymentsTab.module.scss
+++ b/src/apps/accounts/src/settings/tabs/payments/PaymentsTab.module.scss
@@ -40,6 +40,22 @@
border-radius: 4px;
margin-top: $sp-8;
+ .confirmSelectionReset {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: $sp-8;
+
+ @include ltelg {
+ flex-direction: column;
+ align-items: flex-start;
+
+ button {
+ margin-top: $sp-4;
+ }
+ }
+ }
+
.providers {
display: grid;
gap: $sp-8;
diff --git a/src/apps/accounts/src/settings/tabs/payments/PaymentsTab.tsx b/src/apps/accounts/src/settings/tabs/payments/PaymentsTab.tsx
index 1159b44f1..a3f0d6781 100644
--- a/src/apps/accounts/src/settings/tabs/payments/PaymentsTab.tsx
+++ b/src/apps/accounts/src/settings/tabs/payments/PaymentsTab.tsx
@@ -35,6 +35,9 @@ const PaymentsTab: FC = (props: PaymentsTabProps) => {
const [hasEmailedSupport, setHasEmailedSupport]: [boolean, Dispatch>]
= useState(false)
+ const [wantsToChangePreviousSelection, setWantsToChangePreviousSelection]: [boolean, Dispatch>]
+ = useState(false)
+
const [paymentService, setPaymentService]: [string | undefined, Dispatch>] = useState(
localStorage.getItem(`${props.profile.handle}_${PAYMENT_PROVIDER_KEY}`) || undefined,
)
@@ -68,7 +71,12 @@ const PaymentsTab: FC = (props: PaymentsTabProps) => {
}
}
- function handleSelectedProviderReset(): void {
+ function handleSelectedProviderChange(): void {
+ setWantsToChangePreviousSelection(true)
+ }
+
+ function handleSelectionReset(): void {
+ setWantsToChangePreviousSelection(false)
localStorage.removeItem(`${props.profile.handle}_${PAYMENT_PROVIDER_KEY}`)
setPaymentService(undefined)
}
@@ -108,7 +116,7 @@ const PaymentsTab: FC = (props: PaymentsTabProps) => {
{
- paymentService ? (
+ paymentService && !wantsToChangePreviousSelection ? (
@@ -123,11 +131,37 @@ const PaymentsTab: FC = (props: PaymentsTabProps) => {
secondary
size='lg'
label='Change Provider'
- onClick={handleSelectedProviderReset}
+ onClick={handleSelectedProviderChange}
/>
)}
/>
- ) : (
+ ) : undefined
+ }
+
+ {
+ paymentService && wantsToChangePreviousSelection ? (
+
+
+
+ Your currently selected payment provider is:
+ {' '}
+ {paymentService}
+
+
+ Note:
+ You have chosen to change your selected payment provider. This change may take up to 48 hours to be reflected in your account.
+
+
+
+ ) : undefined
+ }
+
+ {
+ !paymentService || wantsToChangePreviousSelection ? (
<>
@@ -214,7 +248,7 @@ const PaymentsTab: FC
= (props: PaymentsTabProps) => {
prior to making a payment provider decision.
>
- )
+ ) : undefined
}
From b377feba567b92bb04e934d1b7a5bf3b2b068e34 Mon Sep 17 00:00:00 2001
From: Kiril Kartunov
Date: Tue, 6 Jun 2023 10:36:56 +0300
Subject: [PATCH 3/3] MP-39 wrap up change username&password section
---
.../src/settings/tabs/AccountSettingsTabs.tsx | 6 +-
.../src/settings/tabs/account/AccountTab.tsx | 5 +-
.../account/user-and-pass/UserAndPassword.tsx | 85 ++++++++++++++++---
.../password-tips/PasswordTips.module.scss | 17 ++++
.../user-and-pass/password-tips/index.tsx | 23 +++++
...onfig.ts => user-and-pass.form.config.tsx} | 25 ++++++
.../ModalTriggerButton.module.scss | 5 ++
.../core/lib/profile/data-providers/index.ts | 1 +
.../profile/data-providers/useMemberTraits.ts | 10 +++
src/libs/core/lib/profile/index.ts | 1 +
.../lib/profile/modify-user-role.model.ts | 12 ++-
.../lib/profile/profile-functions/index.ts | 2 +
.../profile-store/profile-endpoint.config.ts | 4 +-
.../profile-store/profile-xhr.store.ts | 29 +++++--
.../profile-functions/profile.functions.ts | 22 ++++-
.../core/lib/profile/user-traits.model.ts | 17 ++++
.../form-input/input-text/InputText.tsx | 44 ++++++----
.../lib/components/form/form-input.model.ts | 15 ++++
18 files changed, 276 insertions(+), 47 deletions(-)
create mode 100644 src/apps/accounts/src/settings/tabs/account/user-and-pass/password-tips/PasswordTips.module.scss
create mode 100644 src/apps/accounts/src/settings/tabs/account/user-and-pass/password-tips/index.tsx
rename src/apps/accounts/src/settings/tabs/account/user-and-pass/{user-and-pass.form.config.ts => user-and-pass.form.config.tsx} (64%)
create mode 100644 src/libs/core/lib/profile/data-providers/useMemberTraits.ts
create mode 100644 src/libs/core/lib/profile/user-traits.model.ts
diff --git a/src/apps/accounts/src/settings/tabs/AccountSettingsTabs.tsx b/src/apps/accounts/src/settings/tabs/AccountSettingsTabs.tsx
index e0a968e3f..2c2b006d4 100644
--- a/src/apps/accounts/src/settings/tabs/AccountSettingsTabs.tsx
+++ b/src/apps/accounts/src/settings/tabs/AccountSettingsTabs.tsx
@@ -1,7 +1,7 @@
import { Dispatch, FC, SetStateAction, useMemo, useState } from 'react'
import { useLocation } from 'react-router-dom'
-import { UserProfile } from '~/libs/core'
+import { useMemberTraits, UserProfile, UserTraits } from '~/libs/core'
import { TabsNavbar } from '~/libs/ui'
import { AccountSettingsTabsConfig, AccountSettingsTabViews, getHashFromTabId, getTabIdFromHash } from './config'
@@ -22,6 +22,8 @@ const AccountSettingsTabs: FC = (props: AccountSetting
const [activeTab, setActiveTab]: [string, Dispatch>]
= useState(activeTabHash)
+ const memberTraits: UserTraits[] | undefined = useMemberTraits(props.profile.handle)
+
function handleTabChange(tabId: string): void {
setActiveTab(tabId)
window.location.hash = getHashFromTabId(tabId)
@@ -36,7 +38,7 @@ const AccountSettingsTabs: FC = (props: AccountSetting
/>
{activeTab === AccountSettingsTabViews.account && (
-
+
)}
{activeTab === AccountSettingsTabViews.preferences && (
diff --git a/src/apps/accounts/src/settings/tabs/account/AccountTab.tsx b/src/apps/accounts/src/settings/tabs/account/AccountTab.tsx
index 3439dbc69..536d321a6 100644
--- a/src/apps/accounts/src/settings/tabs/account/AccountTab.tsx
+++ b/src/apps/accounts/src/settings/tabs/account/AccountTab.tsx
@@ -1,6 +1,6 @@
import { FC } from 'react'
-import { UserProfile } from '~/libs/core'
+import { UserProfile, UserTraits } from '~/libs/core'
import { AccountRole } from './account-role'
import { SecuritySection } from './security'
@@ -9,6 +9,7 @@ import styles from './AccountTab.module.scss'
interface AccountTabProps {
profile: UserProfile
+ memberTraits: UserTraits[] | undefined
}
const AccountTab: FC = (props: AccountTabProps) => (
@@ -17,7 +18,7 @@ const AccountTab: FC = (props: AccountTabProps) => (
-
+
diff --git a/src/apps/accounts/src/settings/tabs/account/user-and-pass/UserAndPassword.tsx b/src/apps/accounts/src/settings/tabs/account/user-and-pass/UserAndPassword.tsx
index d1eec06c2..d41821722 100644
--- a/src/apps/accounts/src/settings/tabs/account/user-and-pass/UserAndPassword.tsx
+++ b/src/apps/accounts/src/settings/tabs/account/user-and-pass/UserAndPassword.tsx
@@ -1,8 +1,14 @@
-/* eslint-disable no-console */
-import { Dispatch, FC, useCallback, useState } from 'react'
+import { Dispatch, FC, useCallback, useEffect, useMemo, useState } from 'react'
+import { has, trim } from 'lodash'
+import { toast } from 'react-toastify'
-import { Collapsible, Form, FormInputModel, FormToggleSwitch } from '~/libs/ui'
-import { UserProfile } from '~/libs/core'
+import {
+ Collapsible,
+ Form,
+ FormInputModel,
+ FormToggleSwitch,
+} from '~/libs/ui'
+import { updateMemberPasswordAsync, updateMemberTraitsAsync, UserProfile, UserTrait, UserTraits } from '~/libs/core'
import { SettingSection } from '~/apps/accounts/src/lib'
import { UserAndPassFromConfig } from './user-and-pass.form.config'
@@ -10,6 +16,7 @@ import styles from './UserAndPassword.module.scss'
interface UserAndPasswordProps {
profile: UserProfile
+ memberTraits: UserTraits[] | undefined
}
const UserAndPassword: FC = (props: UserAndPasswordProps) => {
@@ -18,32 +25,82 @@ const UserAndPassword: FC = (props: UserAndPasswordProps)
handle: props.profile.handle,
})
+ const personalizationTrait: UserTraits | undefined = useMemo(
+ () => props.memberTraits?.find((trait: UserTraits) => trait.traitId === 'personalization'),
+ [props.memberTraits],
+ )
+
const [userConsent, setUserConsent]: [boolean, Dispatch] = useState(false)
const requestGenerator: (inputs: ReadonlyArray) => any
= useCallback((inputs: ReadonlyArray) => {
- console.log('inputs', inputs)
- return {}
- }, [])
+ const currentPassword: any = inputs[2]
+ const newPassword: any = inputs[3]
+
+ return {
+ currentPassword: currentPassword.value,
+ newPassword: newPassword.value,
+ userId: props.profile.userId,
+ }
+ }, [props.profile.userId])
- async function onSave(val: any): Promise {
- console.log('onSave', val)
+ useEffect(() => {
+ if (personalizationTrait) {
+ setUserConsent(
+ !!personalizationTrait?.traits.data.find(
+ (trait: UserTrait) => has(trait, 'userConsent') && trait.userConsent === true,
+ ),
+ )
+ }
+ }, [personalizationTrait])
+
+ async function onSave(request: any): Promise {
+ await updateMemberPasswordAsync(request.userId, request.currentPassword, request.newPassword)
}
- function handleUserConsentChange(event: any): void {
- console.log('handleUserConsentChange', event)
- setUserConsent(!userConsent)
+ function handleUserConsentChange(): void {
+ updateMemberTraitsAsync(props.profile.handle, [{
+ categoryName: 'Personalization',
+ traitId: 'personalization',
+ traits: {
+ data: [{
+ userConsent: !userConsent,
+ }],
+ },
+ }])
+ .then(() => {
+ setUserConsent(!userConsent)
+ toast.success('User consent updated successfully.')
+ })
+ .catch(() => {
+ toast.error('Failed to update user consent.')
+ })
}
function shouldDisableChangePasswordButton(): boolean {
// pass reset form validation
- console.log('formValues', formValues)
+ const specialChars: any = /[`!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/
+ const currentPassword: any = formValues[2]
+ const newPassword: any = formValues[3]
+ const reTypeNewPassword: any = formValues[4]
+
+ if (
+ trim(currentPassword?.value)
+ && trim(newPassword?.value)
+ && newPassword.value?.length >= 8
+ && (
+ /\d/.test(newPassword?.value) || specialChars.test(newPassword?.value)
+ )
+ && newPassword?.value !== currentPassword?.value
+ && trim(reTypeNewPassword?.value)
+ && newPassword?.value === reTypeNewPassword?.value) {
+ return false
+ }
return true
}
function setChangePasswordFormValues(val: any): void {
- console.log('setChangePasswordFormValues', val)
setFormValues({
...formValues,
...val,
diff --git a/src/apps/accounts/src/settings/tabs/account/user-and-pass/password-tips/PasswordTips.module.scss b/src/apps/accounts/src/settings/tabs/account/user-and-pass/password-tips/PasswordTips.module.scss
new file mode 100644
index 000000000..8be5e0114
--- /dev/null
+++ b/src/apps/accounts/src/settings/tabs/account/user-and-pass/password-tips/PasswordTips.module.scss
@@ -0,0 +1,17 @@
+@import '@libs/ui/styles/includes';
+
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+
+ .infoText {
+ margin-bottom: $sp-3;
+ font-size: 14px;
+ font-weight: $font-weight-bold;
+ }
+
+ .tip {
+ font-size: 14px;
+ }
+}
diff --git a/src/apps/accounts/src/settings/tabs/account/user-and-pass/password-tips/index.tsx b/src/apps/accounts/src/settings/tabs/account/user-and-pass/password-tips/index.tsx
new file mode 100644
index 000000000..e3553e7fa
--- /dev/null
+++ b/src/apps/accounts/src/settings/tabs/account/user-and-pass/password-tips/index.tsx
@@ -0,0 +1,23 @@
+import { FC } from 'react'
+
+import styles from './PasswordTips.module.scss'
+
+type PasswordTip = { text: string, valid: boolean }
+
+interface PasswordTipsProps {
+ infoText: string
+ tips: Array
+}
+
+const PasswordTips: FC = (props: PasswordTipsProps) => (
+
+
{props.infoText}
+ {
+ props.tips.map((tip: PasswordTip) => (
+
{tip.text}
+ ))
+ }
+
+)
+
+export default PasswordTips
diff --git a/src/apps/accounts/src/settings/tabs/account/user-and-pass/user-and-pass.form.config.ts b/src/apps/accounts/src/settings/tabs/account/user-and-pass/user-and-pass.form.config.tsx
similarity index 64%
rename from src/apps/accounts/src/settings/tabs/account/user-and-pass/user-and-pass.form.config.ts
rename to src/apps/accounts/src/settings/tabs/account/user-and-pass/user-and-pass.form.config.tsx
index aa3d0cb71..7b2ae8fe1 100644
--- a/src/apps/accounts/src/settings/tabs/account/user-and-pass/user-and-pass.form.config.ts
+++ b/src/apps/accounts/src/settings/tabs/account/user-and-pass/user-and-pass.form.config.tsx
@@ -2,6 +2,8 @@ import { noop } from 'lodash'
import { FormDefinition, validatorRequired } from '~/libs/ui'
+import PasswordTips from './password-tips'
+
export const UserAndPassFromConfig: FormDefinition = {
buttons: {
primaryGroup: [],
@@ -46,6 +48,19 @@ export const UserAndPassFromConfig: FormDefinition = {
label: 'New Password',
name: 'newPassword',
placeholder: 'Type your new password',
+ tooltip: {
+ className: 'passTooltip',
+ content: ,
+ place: 'bottom',
+ },
type: 'password',
validators: [
{
@@ -58,6 +73,16 @@ export const UserAndPassFromConfig: FormDefinition = {
label: 'Re-Type New Password',
name: 'reTypeNewPassword',
placeholder: 'Re-Type New password',
+ tooltip: {
+ className: 'passTooltip',
+ content: ,
+ place: 'bottom',
+ },
type: 'password',
validators: [
{
diff --git a/src/apps/profiles/src/member-profile/tc-activity/ModalTriggerButton/ModalTriggerButton.module.scss b/src/apps/profiles/src/member-profile/tc-activity/ModalTriggerButton/ModalTriggerButton.module.scss
index f94510be0..73fc8f2fd 100644
--- a/src/apps/profiles/src/member-profile/tc-activity/ModalTriggerButton/ModalTriggerButton.module.scss
+++ b/src/apps/profiles/src/member-profile/tc-activity/ModalTriggerButton/ModalTriggerButton.module.scss
@@ -4,4 +4,9 @@
border: 1px solid $black-10;
padding: $sp-1 $sp-4 !important;
margin-left: $sp-2;
+
+ svg {
+ width: 24px;
+ height: 24px;
+ }
}
\ No newline at end of file
diff --git a/src/libs/core/lib/profile/data-providers/index.ts b/src/libs/core/lib/profile/data-providers/index.ts
index 861433b32..152d2f55d 100644
--- a/src/libs/core/lib/profile/data-providers/index.ts
+++ b/src/libs/core/lib/profile/data-providers/index.ts
@@ -8,3 +8,4 @@ export * from './useStatsDistribution'
export * from './useMemberEmailPreferences'
export * from './useMemberMFAStatus'
export * from './useDiceIdConnection'
+export * from './useMemberTraits'
diff --git a/src/libs/core/lib/profile/data-providers/useMemberTraits.ts b/src/libs/core/lib/profile/data-providers/useMemberTraits.ts
new file mode 100644
index 000000000..3d0cd5983
--- /dev/null
+++ b/src/libs/core/lib/profile/data-providers/useMemberTraits.ts
@@ -0,0 +1,10 @@
+import useSWR, { SWRResponse } from 'swr'
+
+import { getProfileUrl } from '../profile-functions'
+import { UserTraits } from '../user-traits.model'
+
+export function useMemberTraits(handle?: string): UserTraits[] | undefined {
+ const { data }: SWRResponse = useSWR(handle ? `${getProfileUrl(handle)}/traits` : undefined)
+
+ return data
+}
diff --git a/src/libs/core/lib/profile/index.ts b/src/libs/core/lib/profile/index.ts
index 12cc8c5ed..1caca04e0 100644
--- a/src/libs/core/lib/profile/index.ts
+++ b/src/libs/core/lib/profile/index.ts
@@ -12,3 +12,4 @@ export * from './modify-user-role.model'
export * from './user-email-preference.model'
export * from './modify-user-email-preferences.model'
export * from './modify-user-mfa.model'
+export * from './user-traits.model'
diff --git a/src/libs/core/lib/profile/modify-user-role.model.ts b/src/libs/core/lib/profile/modify-user-role.model.ts
index 059900bf9..b2c75d9c5 100644
--- a/src/libs/core/lib/profile/modify-user-role.model.ts
+++ b/src/libs/core/lib/profile/modify-user-role.model.ts
@@ -1,13 +1,17 @@
-export interface ModifyUserRoleRequest {
+export interface ModifyUserPropertyRequest {
param: {
- primaryRole: string
+ primaryRole?: string
+ credential?: {
+ currentPassword: string
+ password: string
+ }
}
}
-export interface ModifyUserRoleResponse {
+export interface ModifyUserPropertyResponse {
id: string
result: {
- content: string
+ content: string | any
success: boolean
}
}
diff --git a/src/libs/core/lib/profile/profile-functions/index.ts b/src/libs/core/lib/profile/profile-functions/index.ts
index e161c476b..2278bfb47 100644
--- a/src/libs/core/lib/profile/profile-functions/index.ts
+++ b/src/libs/core/lib/profile/profile-functions/index.ts
@@ -9,6 +9,8 @@ export {
updatePrimaryMemberRoleAsync,
updateMemberEmailPreferencesAsync,
updateMemberMFAStatusAsync,
+ updateMemberPasswordAsync,
+ updateMemberTraitsAsync,
} from './profile.functions'
export * from './profile-store'
export * from './rating.functions'
diff --git a/src/libs/core/lib/profile/profile-functions/profile-store/profile-endpoint.config.ts b/src/libs/core/lib/profile/profile-functions/profile-store/profile-endpoint.config.ts
index 20956b134..620163805 100644
--- a/src/libs/core/lib/profile/profile-functions/profile-store/profile-endpoint.config.ts
+++ b/src/libs/core/lib/profile/profile-functions/profile-store/profile-endpoint.config.ts
@@ -26,8 +26,8 @@ export function memberStatsDistroURL(): string {
return `${EnvironmentConfig.API.V3}/members/stats/distribution`
}
-export function memberModifyRoleURL(): string {
- return `${EnvironmentConfig.API.V3}/users/updatePrimaryRole`
+export function memberModifyURL(): string {
+ return `${EnvironmentConfig.API.V3}/users`
}
export function memberEmailPreferencesURL(): string {
diff --git a/src/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store.ts b/src/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store.ts
index fc5765af7..664f2b129 100644
--- a/src/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store.ts
+++ b/src/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store.ts
@@ -3,17 +3,18 @@ import { CountryLookup } from '../../country-lookup.model'
import { EditNameRequest } from '../../edit-name-request.model'
import { ModifyMemberEmailPreferencesRequest } from '../../modify-user-email-preferences.model'
import { ModifyUserMFARequest, ModifyUserMFAResponse } from '../../modify-user-mfa.model'
-import { ModifyUserRoleRequest, ModifyUserRoleResponse } from '../../modify-user-role.model'
+import { ModifyUserPropertyRequest, ModifyUserPropertyResponse } from '../../modify-user-role.model'
import { UserEmailPreferences } from '../../user-email-preference.model'
import { UserProfile } from '../../user-profile.model'
import { UserStats } from '../../user-stats.model'
+import { UserTraits } from '../../user-traits.model'
import { UserVerify } from '../../user-verify.model'
import {
countryLookupURL,
memberEmailPreferencesURL,
memberModifyMfaURL,
- memberModifyRoleURL,
+ memberModifyURL,
profile as profileUrl,
verify as verifyUrl,
} from './profile-endpoint.config'
@@ -43,9 +44,9 @@ export function getCountryLookup(): Promise {
.then((countryLookup: any) => countryLookup.result?.content || [])
}
-export async function updatePrimaryMemberRole(primaryRole: string): Promise {
- return xhrPostAsync(
- memberModifyRoleURL(),
+export async function updatePrimaryMemberRole(primaryRole: string): Promise {
+ return xhrPostAsync(
+ `${memberModifyURL()}/updatePrimaryRole`,
{ param: { primaryRole } },
)
}
@@ -66,3 +67,21 @@ export async function updateMemberMFA(userId: number, payload: ModifyUserMFARequ
payload,
)
}
+
+export async function updateMemberPassword(
+ userId: number,
+ currentPassword: string,
+ newPassword: string,
+): Promise {
+ return xhrPatchAsync(
+ `${memberModifyURL()}/${userId}`,
+ { param: { credential: { currentPassword, password: newPassword } } },
+ )
+}
+
+export async function updateMemberTraits(
+ handle: string,
+ traits: UserTraits[],
+): Promise {
+ return xhrPutAsync(`${profileUrl(handle)}/traits`, traits)
+}
diff --git a/src/libs/core/lib/profile/profile-functions/profile.functions.ts b/src/libs/core/lib/profile/profile-functions/profile.functions.ts
index fc079534c..70f3e8b73 100644
--- a/src/libs/core/lib/profile/profile-functions/profile.functions.ts
+++ b/src/libs/core/lib/profile/profile-functions/profile.functions.ts
@@ -3,10 +3,11 @@ import { CountryLookup } from '../country-lookup.model'
import { EditNameRequest } from '../edit-name-request.model'
import { ModifyMemberEmailPreferencesRequest } from '../modify-user-email-preferences.model'
import { ModifyUserMFARequest, ModifyUserMFAResponse } from '../modify-user-mfa.model'
-import { ModifyUserRoleResponse } from '../modify-user-role.model'
+import { ModifyUserPropertyResponse } from '../modify-user-role.model'
import { UserEmailPreferences } from '../user-email-preference.model'
import { UserProfile } from '../user-profile.model'
import { UserStats } from '../user-stats.model'
+import { UserTraits } from '../user-traits.model'
import { UserVerify } from '../user-verify.model'
import { profileFactoryCreate } from './profile-factory'
@@ -15,6 +16,8 @@ import {
getCountryLookup,
updateMemberEmailPreferences,
updateMemberMFA,
+ updateMemberPassword,
+ updateMemberTraits,
updatePrimaryMemberRole,
} from './profile-store/profile-xhr.store'
@@ -73,7 +76,7 @@ export async function getCountryLookupAsync(): Promise {
return getCountryLookup()
}
-export async function updatePrimaryMemberRoleAsync(primaryRole: string): Promise {
+export async function updatePrimaryMemberRoleAsync(primaryRole: string): Promise {
return updatePrimaryMemberRole(primaryRole)
}
@@ -90,3 +93,18 @@ export async function updateMemberMFAStatusAsync(
): Promise {
return updateMemberMFA(userId, payload)
}
+
+export async function updateMemberPasswordAsync(
+ userId: number,
+ currentPassword: string,
+ newPassword: string,
+): Promise {
+ return updateMemberPassword(userId, currentPassword, newPassword)
+}
+
+export async function updateMemberTraitsAsync(
+ handle: string,
+ traits: UserTraits[],
+): Promise {
+ return updateMemberTraits(handle, traits)
+}
diff --git a/src/libs/core/lib/profile/user-traits.model.ts b/src/libs/core/lib/profile/user-traits.model.ts
new file mode 100644
index 000000000..7c8940387
--- /dev/null
+++ b/src/libs/core/lib/profile/user-traits.model.ts
@@ -0,0 +1,17 @@
+export type UserTrait = {
+ [key: string]: any
+}
+
+export type UserTraits = {
+ categoryName: string
+ createdAt?: number
+ createdBy?: number
+ traitId: string
+ traits: {
+ data: Array
+ traitId?: string
+ }
+ updatedAt?: number
+ updatedBy?: number
+ userId?: number
+}
diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-text/InputText.tsx b/src/libs/ui/lib/components/form/form-groups/form-input/input-text/InputText.tsx
index 929cbe4c8..dad7f6afe 100644
--- a/src/libs/ui/lib/components/form/form-groups/form-input/input-text/InputText.tsx
+++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-text/InputText.tsx
@@ -1,9 +1,10 @@
import { FC, FocusEvent } from 'react'
import cn from 'classnames'
-import { InputValue } from '../../../form-input.model'
+import { FormInputTooltipOptions, InputValue } from '../../../form-input.model'
import { FormInputAutocompleteOption } from '../form-input-autcomplete-option.enum'
import { InputWrapper } from '../input-wrapper'
+import { Tooltip } from '../../../../tooltip'
import styles from './InputText.module.scss'
@@ -26,6 +27,7 @@ export interface InputTextProps {
readonly readonly?: boolean
readonly spellCheck?: boolean
readonly tabIndex: number
+ readonly tooltip?: FormInputTooltipOptions
readonly type: InputTextTypes
readonly value?: InputValue
}
@@ -36,6 +38,24 @@ const InputText: FC = (props: InputTextProps) => {
? 'on'
: props.value as string | number | undefined
+ const renderInput: () => JSX.Element = () => (
+
+ )
+
return (
= (props: InputTextProps) => {
label={props.label || props.name}
hideInlineErrors={props.hideInlineErrors}
>
-
+ {
+ props.tooltip ? (
+ {renderInput()}
+ ) : (
+ renderInput()
+ )
+ }
)
}
diff --git a/src/libs/ui/lib/components/form/form-input.model.ts b/src/libs/ui/lib/components/form/form-input.model.ts
index 315761b8a..ff19c7e3e 100644
--- a/src/libs/ui/lib/components/form/form-input.model.ts
+++ b/src/libs/ui/lib/components/form/form-input.model.ts
@@ -1,3 +1,5 @@
+import { ReactNode } from 'react'
+
import { FormInputAutocompleteOption } from './form-groups'
import { InputEvent } from './form-input.event'
import { ValidatorFn } from './validator-functions'
@@ -27,6 +29,18 @@ export interface FormCard {
export type InputValue = string | boolean | FileList | undefined
+export type FormInputTooltipOptions = {
+ className?: string
+ content?: ReactNode
+ /** Set clickable=true to allows interactions with the tooltip */
+ clickable?: boolean
+ disableWrap?: boolean
+ place?: 'top' | 'right' | 'bottom' | 'left'
+ children?: ReactNode
+ triggerOn?: 'click' | 'hover'
+ strategy?: 'absolute' | 'fixed'
+}
+
export interface FormInputModel {
readonly autocomplete?: FormInputAutocompleteOption
readonly cards?: ReadonlyArray
@@ -54,6 +68,7 @@ export interface FormInputModel {
readonly readonly?: boolean
readonly spellCheck?: boolean
readonly title?: string
+ tooltip?: FormInputTooltipOptions
touched?: boolean
// eslint-disable-next-line max-len
readonly type: 'card-set'| 'checkbox' | 'password' | 'radio' | 'rating' | 'text' | 'textarea' | 'image-picker' | 'toggle'