Skip to content

Commit

Permalink
LBGG-564: Account confirmation (#580)
Browse files Browse the repository at this point in the history
* Update version requirements in `package`

* Update API library

* Create `useConfirmAccount` composable

* Create `BasicAlert` component, and add it to the default layout

* Create `Loader` component

* Create `/confirm-account` route

* Fix rendering error messages on signup

* Actually fix rendering errors on signup failure

* Fix confrimation aborting, when no code or from path mismatch

* Switch composable definitions to use function declarations over function expressions

* Pin the latest `pnpm` version
  • Loading branch information
erunks authored Nov 29, 2023
1 parent 88298c9 commit ab475f1
Show file tree
Hide file tree
Showing 33 changed files with 594 additions and 30 deletions.
1 change: 1 addition & 0 deletions assets/sprite/svg/circle-check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/sprite/svg/circle-exclamation.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/sprite/svg/circle-info.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/sprite/svg/spinner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/sprite/svg/triangle-exclamation.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ export {}

declare module 'vue' {
export interface GlobalComponents {
ISvgCircleCheck: typeof import('~icons/svg/circle-check')['default']
ISvgCircleExclamation: typeof import('~icons/svg/circle-exclamation')['default']
ISvgCircleInfo: typeof import('~icons/svg/circle-info')['default']
ISvgClock: typeof import('~icons/svg/clock')['default']
ISvgClose: typeof import('~icons/svg/close')['default']
ISvgEyeHidden: typeof import('~icons/svg/eye-hidden')['default']
ISvgEyeVisible: typeof import('~icons/svg/eye-visible')['default']
ISvgMenu: typeof import('~icons/svg/menu')['default']
ISvgSearch: typeof import('~icons/svg/search')['default']
ISvgSpinner: typeof import('~icons/svg/spinner')['default']
ISvgTriangleExclamation: typeof import('~icons/svg/triangle-exclamation')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
Expand Down
31 changes: 31 additions & 0 deletions components/blocks/Loader/Loader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script setup lang="ts"></script>

<template>
<div class="loader__container">
<i-svg-spinner class="loader__spinner" />
</div>
</template>

<style lang="postcss" scoped>
.loader__container {
@apply flex justify-center items-center h-screen w-full;
}
.loader__spinner {
@apply h-20 w-20 fill-current;
animation: spin 1.25s steps(9, end) infinite;
@media (prefers-reduced-motion) {
animation-duration: 9s;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
</style>
60 changes: 60 additions & 0 deletions components/blocks/cards/BasicAlert/BasicAlert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { mount, enableAutoUnmount } from '@vue/test-utils'
import { useModalAlert } from 'composables/useModalAlert'
import { getByClass, getByTestId, getHTMLElement } from 'root/testUtils'
import BasicAlert from './BasicAlert.vue'

function getBasicAlertWrapper() {
return mount(BasicAlert)
}

beforeEach(() => {
const modalAlertState = useModalAlert()
modalAlertState.value = {
body: 'This is a test',
show: true,
title: 'A test alert?',
type: 'info',
}
})

enableAutoUnmount(afterEach)

afterEach(() => {
fetchMock.resetMocks()
vi.restoreAllMocks()
})

describe('<BasicAlert />', () => {
it('should render without crashing', () => {
const wrapper = getBasicAlertWrapper()

expect(wrapper.isVisible()).toBe(true)
})

it('renders with the correct information', () => {
const wrapper = getBasicAlertWrapper()

expect(
getHTMLElement(getByClass(wrapper, 'basic-modal-alert__header'))
.childElementCount,
).toEqual(3)
expect(getByClass(wrapper, 'basic-modal-alert__title').text()).toEqual(
'A test alert?',
)
expect(getByClass(wrapper, 'basic-modal-alert__body').text()).toEqual(
'This is a test',
)
})

describe('when the close button is clicked', () => {
it('should emit the close event', async () => {
const wrapper = getBasicAlertWrapper()

await getByTestId(wrapper, 'basic-modal-alert-close-button').trigger(
'click',
)

expect(wrapper.isVisible()).toBe(false)
})
})
})
99 changes: 99 additions & 0 deletions components/blocks/cards/BasicAlert/BasicAlert.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script setup lang="ts">
import BaseModal from 'elements/modals/BaseModal/BaseModal.vue'
import CloseButton from 'elements/buttons/CloseButton/CloseButton.vue'
import Card from 'elements/cards/Card/Card.vue'
import CardHeader from 'elements/cards/CardHeader/CardHeader.vue'
import CardBody from 'elements/cards/CardBody/CardBody.vue'
import { useModalAlert } from 'composables/useModalAlert'
const modalAlertState = useModalAlert()?.value
function close() {
modalAlertState.body = ''
modalAlertState.show = false
modalAlertState.title = ''
modalAlertState.type = ''
}
</script>

<template>
<transition
v-if="modalAlertState.show"
enter-active-class="transition-opacity duration-200"
leave-active-class="transition-opacity duration-200"
enter-to-class="opacity-100"
leave-to-class="opacity-0"
>
<BaseModal v-show="modalAlertState.show" @close="close">
<Card
id="basicModalAlert"
:class="['basic-modal-alert', modalAlertState.type]"
data-testid="basic-modal-alert"
>
<CardHeader class="basic-modal-alert__header">
<i-svg-circle-info v-if="modalAlertState.type === 'info'" />
<i-svg-circle-check v-if="modalAlertState.type === 'success'" />
<i-svg-circle-exclamation v-if="modalAlertState.type === 'error'" />
<i-svg-triangle-exclamation
v-if="modalAlertState.type === 'warning'"
/>
<h2 class="basic-modal-alert__title">
{{ modalAlertState.title }}
</h2>
<CloseButton
class="basic-modal-alert__close-button"
data-testid="basic-modal-alert-close-button"
@click.prevent="close"
/>
</CardHeader>
<CardBody class="basic-modal-alert__body">
{{ modalAlertState.body }}
</CardBody>
</Card>
</BaseModal>
</transition>
</template>

<style lang="postcss" scoped>
.basic-modal-alert {
@apply bg-white w-full max-w-xl;
svg {
@apply h-5 w-5 mr-2;
}
&.info svg {
@apply fill-blue-500;
}
&.success svg {
@apply fill-green-500;
}
&.error svg {
@apply fill-red-500;
}
&.warning svg {
@apply fill-yellow-500;
}
& .basic-modal-alert__header {
@apply flex flex-row items-center;
}
& .basic-modal-alert__title,
& .basic-modal-alert__body {
@apply flex flex-1;
}
& .basic-modal-alert__title {
@apply text-lg font-medium py-2;
@apply flex items-center;
}
& .basic-modal-alert__body {
@apply justify-center m-8;
}
}
</style>
11 changes: 5 additions & 6 deletions components/blocks/cards/SignUpCard/SignUpCard.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { type Ref, ref } from 'vue'
import { sentenceCase } from 'lib/helpers'
import BaseInput from 'elements/inputs/BaseInput/BaseInput.vue'
import HideShowPassword from 'elements/buttons/HideShowPassword/HideShowPassword.vue'
import BaseButton from 'elements/buttons/BaseButton/BaseButton.vue'
Expand Down Expand Up @@ -98,12 +99,10 @@ async function signup() {
},
{
onError: (val: any) => {
errorText.value = `Error(s): ${(
Object.values(val.error.errors) as string[]
).reduce(
(accumulator: string, val: string) => `${accumulator} ${val}`,
'',
)}`
const errors = Object.values(val.error?.errors) as string[][]
errorText.value = `Error(s): ${errors
.map((errorType) => errorType.map(sentenceCase))
.join(', ')}`
showErrorsText.value = true
},
onOkay: () => {
Expand Down
1 change: 1 addition & 0 deletions composables/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as useConfirmAccount } from './useConfirmAccount'
export { default as useGetLeaderboardDetail } from './useGetLeaderboardDetail'
export { default as useGetUserDetail } from './useGetUserDetail'
export { default as useLoginUser } from './useLoginUser'
Expand Down
20 changes: 20 additions & 0 deletions composables/api/useConfirmAccount/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ApiResponse, optionalParameters, useApi } from 'composables/useApi'
import { Account } from 'lib/api/Account'

export async function useConfirmAccount(
confirmationToken: string,
opts: optionalParameters<void> = {},
): Promise<ApiResponse<void>> {
const { onError, onOkay } = opts

const account = new Account({
baseUrl: useRuntimeConfig().public.BACKEND_BASE_URL,
})

return await useApi<void>(
async () => await account.confirmUpdate(confirmationToken),
{ onError, onOkay },
)
}

export default useConfirmAccount
24 changes: 24 additions & 0 deletions composables/api/useConfirmAccount/useConfirmAccount.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useConfirmAccount } from '.'

const mockSuccessAccountConfirmation = vi.fn(() =>
Promise.resolve({ ok: true }),
)

describe('useConfirmUser', () => {
describe('when everything is successful', () => {
const confirmationCode = '123'

it('creates a PUT request to confirm the account', async () => {
vi.mock('lib/api/Account', () => ({
Account: function Account() {
this.confirmUpdate = mockSuccessAccountConfirmation
},
}))

await useConfirmAccount(confirmationCode)

expect(mockSuccessAccountConfirmation).toBeCalledTimes(1)
expect(mockSuccessAccountConfirmation).toBeCalledWith(confirmationCode)
})
})
})
4 changes: 2 additions & 2 deletions composables/api/useGetLeaderboardDetail/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { ApiResponse, optionalParameters, useApi } from 'composables/useApi'
import { Leaderboards } from 'lib/api/Leaderboards'
import type { LeaderboardViewModel } from 'lib/api/data-contracts'

export const useGetLeaderboardDetail = async (
export async function useGetLeaderboardDetail(
leaderboardSlug: string,
opts: optionalParameters<LeaderboardViewModel> = {},
): Promise<ApiResponse<LeaderboardViewModel>> => {
): Promise<ApiResponse<LeaderboardViewModel>> {
const { onError, onOkay } = opts
const responseData = ref<LeaderboardViewModel>({
categories: [],
Expand Down
4 changes: 2 additions & 2 deletions composables/api/useGetUserDetail/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { ApiResponse, optionalParameters, useApi } from 'composables/useApi'
import { Users } from 'lib/api/Users'
import type { UserViewModel } from 'lib/api/data-contracts'

export const useGetUserDetail = async (
export async function useGetUserDetail(
userId: string,
opts: optionalParameters<UserViewModel> = {},
): Promise<ApiResponse<UserViewModel>> => {
): Promise<ApiResponse<UserViewModel>> {
const { onError, onOkay } = opts
const responseData = ref<UserViewModel>({
id: '',
Expand Down
4 changes: 2 additions & 2 deletions composables/api/useLoginUser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import type {
UserViewModel,
} from 'lib/api/data-contracts'

export const useLoginUser = async (
export async function useLoginUser(
requestData: LoginRequest,
opts: optionalParameters<UserViewModel> = {},
): Promise<ApiResponse<LoginResponse>> => {
): Promise<ApiResponse<LoginResponse>> {
const { onError, onOkay } = opts
const authToken = useSessionToken()
const currentUser = useCurrentUser()
Expand Down
2 changes: 1 addition & 1 deletion composables/api/useLogoutUser/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCurrentUser } from 'composables/useCurrentUser'
import { useSessionToken } from 'composables/useSessionToken'

export const useLogoutUser = (): void => {
export function useLogoutUser(): void {
const authToken = useSessionToken()
const currentUser = useCurrentUser()

Expand Down
4 changes: 2 additions & 2 deletions composables/api/useRegisterUser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { ApiResponse, optionalParameters, useApi } from 'composables/useApi'
import { Account } from 'lib/api/Account'
import type { RegisterRequest, UserViewModel } from 'lib/api/data-contracts'

export const useRegisterUser = async (
export async function useRegisterUser(
requestData: RegisterRequest,
opts: optionalParameters<UserViewModel> = {},
): Promise<ApiResponse<UserViewModel>> => {
): Promise<ApiResponse<UserViewModel>> {
const { onError, onOkay } = opts
const responseData = ref<UserViewModel>({
id: '',
Expand Down
4 changes: 2 additions & 2 deletions composables/useApi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ export interface optionalParameters<T> {
* @param {optionalParameters<T>} [opts = {}] - optional parameters. `onOkay`: a `function` to call when the API request succeeds. `responseData`: a `ref` to set the data to when the API request succeeds. If not passed it, a ref is created on a successful API call
* @returns {Promise<ApiResponse<T>>} returns an `ApiResponse` object, but the `data` property is not guaranteed to be present
*/
export const useApi = async <T>(
export async function useApi<T>(
apiRequest: () => Promise<HttpResponse<T, void | ProblemDetails>>,
opts: optionalParameters<T> = {},
): Promise<ApiResponse<T>> => {
): Promise<ApiResponse<T>> {
const responseError = ref<ProblemDetails | null | void>(null)
const responseErrors = ref<ValidationProblemDetails | null | void>(null)
const responseLoading = ref(true)
Expand Down
5 changes: 3 additions & 2 deletions composables/useCurrentUser.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { UserViewModel } from 'lib/api/data-contracts'

export const useCurrentUser = () =>
useState<UserViewModel>('current_user', () => ({
export function useCurrentUser() {
return useState<UserViewModel>('current_user', () => ({
id: '',
username: '',
}))
}

export default useCurrentUser
Loading

0 comments on commit ab475f1

Please sign in to comment.