diff --git a/src/App/redirects/ActivationRedirect.tsx b/src/App/redirects/ActivationRedirect.tsx new file mode 100644 index 00000000..aaa7f7a0 --- /dev/null +++ b/src/App/redirects/ActivationRedirect.tsx @@ -0,0 +1,32 @@ +import { + generatePath, + useNavigate, + useParams, +} from 'react-router-dom'; + +import routes from '#routes'; + +interface ActivationParams { + userId: string | undefined; + token: string | undefined; + [key: string]: string | undefined; +} + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const { userId, token } = useParams(); + const navigate = useNavigate(); + + const activationLink = (userId && token && routes.activation.path) ? ({ + pathname: (generatePath(routes.activation.path, { userId, token })), + }) + : routes.pageNotFound.path; + + if (activationLink) { + navigate(activationLink); + } + + return null; +} + +Component.displayName = 'ActivationRedirect'; diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index bfc7e55a..dd24ba4a 100644 --- a/src/App/routes/index.tsx +++ b/src/App/routes/index.tsx @@ -255,7 +255,7 @@ const recoverAccount = customWrapRoute({ }, }); -const resendValidationEmail = customWrapRoute({ +/* const resendValidationEmail = customWrapRoute({ parent: rootLayout, path: 'resend-validation-email', component: { @@ -267,7 +267,7 @@ const resendValidationEmail = customWrapRoute({ title: 'Resend Validation Email', visibility: 'is-not-authenticated', }, -}); +}); */ const cookiePolicy = customWrapRoute({ parent: rootLayout, @@ -310,6 +310,33 @@ const resetPasswordRedirect = customWrapRoute({ visibility: 'is-not-authenticated', }, }); +const activation = customWrapRoute({ + parent: rootLayout, + path: 'activation/:userId/:token', + component: { + render: () => import('#views/Activation'), + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'Activation', + visibility: 'anything', + }, +}); + +const activationRedirect = customWrapRoute({ + parent: rootLayout, + path: 'permalink/user-activation/:userId/:token', + component: { + render: () => import('../redirects/ActivationRedirect'), + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'Activation Redirect', + visibility: 'anything', + }, +}); const wrappedRoutes = { rootLayout, @@ -325,7 +352,7 @@ const wrappedRoutes = { pageNotFound, login, recoverAccount, - resendValidationEmail, + // resendValidationEmail, mySubscription, cookiePolicy, register, @@ -333,6 +360,8 @@ const wrappedRoutes = { subscriptionDetail, recoverAccountConfirm, resetPasswordRedirect, + activationRedirect, + activation, }; export const unwrappedRoutes = unwrapRoute(Object.values(wrappedRoutes)); diff --git a/src/views/Activation/i18n.json b/src/views/Activation/i18n.json new file mode 100644 index 00000000..c09a861b --- /dev/null +++ b/src/views/Activation/i18n.json @@ -0,0 +1,9 @@ +{ + "namespace": "activation", + "strings": { + "activationSuccessMessage":"Your account has been successfully activated!", + "goToLogin":"Go to Login", + "activationFailMessage":"An error occurred during activation. Please try again." + } +} + diff --git a/src/views/Activation/index.tsx b/src/views/Activation/index.tsx new file mode 100644 index 00000000..b6b63521 --- /dev/null +++ b/src/views/Activation/index.tsx @@ -0,0 +1,103 @@ +import { + useEffect, + useState, +} from 'react'; +import { useParams } from 'react-router-dom'; +import { + gql, + useMutation, +} from '@apollo/client'; +import { Message } from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; + +import Link from '#components/Link'; +import Page from '#components/Page'; +import useAlert from '#hooks/useAlert'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +const ACCOUNT_ACTIVATION_MUTATION = gql` + mutation AccountActivation($data: UserActivationInput!) { + public { + accountActivation(data: $data) { + errors + ok + } + } + } +`; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const { userId, token } = useParams<{ userId?: string, token?: string }>(); + const alert = useAlert(); + const strings = useTranslation(i18n); + const [isErrored, setIsError] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(false); + const [ + activate, + ] = useMutation(ACCOUNT_ACTIVATION_MUTATION, { + onCompleted: (response) => { + const activateRes = response?.public?.accountActivation; + if (!response) { + return; + } + if (activateRes.ok) { + setIsSubmitted(true); + } else { + setIsError(true); + } + }, + onError: () => { + alert.show( + strings.activationFailMessage, + { variant: 'danger' }, + ); + }, + }); + + useEffect(() => { + if (userId && token) { + activate({ + variables: { + data: { + uuid: userId, + token, + }, + }, + }); + } + }, [token, activate, userId]); + + if (isSubmitted) { + return ( + + +
+ + {strings.goToLogin} + +
+ +
+ ); + } + if (isErrored) { + return ( + + + + ); + } + return null; +} + +Component.displayName = 'Activation'; diff --git a/src/views/Activation/styles.module.css b/src/views/Activation/styles.module.css new file mode 100644 index 00000000..2b4e1630 --- /dev/null +++ b/src/views/Activation/styles.module.css @@ -0,0 +1,8 @@ +.activation { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-xs); + align-items: center; + text-align: center; + +} \ No newline at end of file diff --git a/src/views/Login/index.tsx b/src/views/Login/index.tsx index b2d8ffbe..4d296cd7 100644 --- a/src/views/Login/index.tsx +++ b/src/views/Login/index.tsx @@ -166,7 +166,7 @@ export function Component() { { signUpLink: ( {strings.loginSignUp} @@ -214,13 +214,13 @@ export function Component() { > {strings.loginForgotUserPass} - {strings.loginResendValidation} - + */}