Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Account Confirmation Resend Button Thingy #609

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions components/blocks/ResendConfirmationBar/ResendConfirmationBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script setup lang="ts">
import { ref } from 'vue'
import BaseButton from 'elements/buttons/BaseButton/BaseButton.vue'
import useResendAccountConfirmation from 'composables/api/useResendAccountConfirmation'
import { useCurrentUser } from 'composables/useCurrentUser'
import { useModalAlert } from 'composables/useModalAlert'

const currentUser = useCurrentUser()
const errorText = ref('')
const showErrorText = ref(false)
const { showAlert } = useModalAlert()

const unconfirmed = ref(
!!currentUser.value?.username && currentUser.value?.role === 'Registered',
)

async function resend() {
await useResendAccountConfirmation({
onError: () => {
errorText.value =
'Something went wrong. Reach out to support if the problem persists'
showErrorText.value = true
},
onOkay: () => {
showAlert({
body: 'If you have a valid account with us, you should receive an email from us soon.',
title: 'Confirmation Request Received',
type: 'info',
})
unconfirmed.value = false
},
})
}
</script>

<template>
<div v-if="unconfirmed" class="resend-confirmation-bar__container">
<div class="resend-confirmation-bar__content">
<div class="resend-confirmation-bar__text">
<i-svg-circle-info />
<span>{{ $t('needToConfirm') }}</span>
</div>
<BaseButton class="resend-confirmation__button" @click="resend">
{{ $t('resendConfirmation') }}
</BaseButton>
</div>
<p v-if="showErrorText" class="error-text">{{ errorText }}</p>
zysim marked this conversation as resolved.
Show resolved Hide resolved
</div>
</template>

<style lang="postcss" scoped>
.resend-confirmation-bar__container,
.resend-confirmation-bar__content {
@apply flex flex-col items-center justify-center;
}

.resend-confirmation-bar__content {
@apply mt-4 gap-y-2 md:flex-row md:gap-x-2;
@apply fill-blue-500;
}

.resend-confirmation-bar__text {
@apply flex items-center gap-x-2;
}

.resend-confirmation__button {
@apply mx-1 py-1 bg-blue-100 hover:bg-blue-300;
zysim marked this conversation as resolved.
Show resolved Hide resolved
}

.error-text {
@apply text-red-600;
}
</style>
4 changes: 3 additions & 1 deletion components/elements/buttons/LogoutButton/LogoutButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import BaseButton from 'elements/buttons/BaseButton/BaseButton.vue'
</script>

<template>
<BaseButton class="logout-button" v-bind="$attrs"> Log Out </BaseButton>
<BaseButton class="logout-button" v-bind="$attrs">
{{ $t('logout') }}
</BaseButton>
</template>

<style lang="postcss" scoped>
Expand Down
1 change: 1 addition & 0 deletions composables/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { default as useLogoutUser } from './useLogoutUser'
export { default as useRecoverAccount } from './useRecoverAccount'
export { default as useRegisterUser } from './useRegisterUser'
export { default as useValidateRecoveryToken } from './useValidateRecoveryToken'
export { default as useResendAccountConfirmation } from './useResendAccountConfirmation'
6 changes: 5 additions & 1 deletion composables/api/useChangePassword/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ApiResponse, optionalParameters, useApi } from 'composables/useApi'
import {
type ApiResponse,
type optionalParameters,
useApi,
} from 'composables/useApi'
import { Account } from 'lib/api/Account'
import type { ChangePasswordRequest } from 'lib/api/data-contracts'

Expand Down
6 changes: 5 additions & 1 deletion composables/api/useConfirmAccount/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ApiResponse, optionalParameters, useApi } from 'composables/useApi'
import {
type ApiResponse,
type optionalParameters,
useApi,
} from 'composables/useApi'
import { Account } from 'lib/api/Account'

export async function useConfirmAccount(
Expand Down
7 changes: 6 additions & 1 deletion composables/api/useGetUserDetail/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { ref } from 'vue'
import { ApiResponse, optionalParameters, useApi } from 'composables/useApi'
import {
type ApiResponse,
type optionalParameters,
useApi,
} from 'composables/useApi'
import { Users } from 'lib/api/Users'
import type { UserViewModel } from 'lib/api/data-contracts'

Expand All @@ -10,6 +14,7 @@ export async function useGetUserDetail(
const { onError, onOkay } = opts
const responseData = ref<UserViewModel>({
id: '',
role: 'Banned',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is Banned the default?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty strings aren't allow based on the type of the property, so by default I assumed Banned would be best since it would act like a user is not signed in

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That feels like a hack. Is it possible to just use an empty object for the whole viewmodel instead? Maybe there's some other pattern that would work here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great example of where typescript hurts more than it helps. You should just be able to initialize it with a null value and then assume that it's not null if the fetch operation succeeds.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the update on this? I believe there was a discussion in chat, right?

Copy link
Contributor Author

@zysim zysim Jul 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just add an empty string to UserRole, tbh. We would know in-code that we're in a logged out state if we see an empty string.

username: '',
})

Expand Down
6 changes: 5 additions & 1 deletion composables/api/useLoginUser/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ApiResponse, optionalParameters, useApi } from 'composables/useApi'
import {
type ApiResponse,
type optionalParameters,
useApi,
} from 'composables/useApi'
import { useCurrentUser } from 'composables/useCurrentUser'
import { useSessionToken } from 'composables/useSessionToken'
import { Account } from 'lib/api/Account'
Expand Down
1 change: 1 addition & 0 deletions composables/api/useLogoutUser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export function useLogoutUser(): void {
authToken.value = ''
currentUser.value = {
id: '',
role: 'Banned',
username: '',
}
}
Expand Down
6 changes: 5 additions & 1 deletion composables/api/useRegisterUser/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { ref } from 'vue'
import { ApiResponse, optionalParameters, useApi } from 'composables/useApi'
import {
type ApiResponse,
type optionalParameters,
useApi,
} from 'composables/useApi'
import { Account } from 'lib/api/Account'
import type { RegisterRequest, UserViewModel } from 'lib/api/data-contracts'

Expand Down
33 changes: 33 additions & 0 deletions composables/api/useResendAccountConfirmation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
useApi,
type ApiResponse,
type optionalParameters,
} from 'composables/useApi'
import { useSessionToken } from 'composables/useSessionToken'
import { Account } from 'lib/api/Account'

/**
* Resends the account confirmation email for a newly-registered user.
*/
export default async function useResendAccountConfirmation(
opts: optionalParameters<void> = {},
): Promise<ApiResponse<void>> {
const { onError, onOkay } = opts
const authToken = useSessionToken()
const account = new Account({
baseUrl: useRuntimeConfig().public.BACKEND_BASE_URL,
})

return await useApi<void>(
async () =>
await account.confirmCreate({
headers: {
Authorization: `Bearer ${authToken.value}`,
},
}),
{
onError,
onOkay,
},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useLoginUser } from '../useLoginUser'
import useResendAccountConfirmation from '.'

const mockSuccessLogin = vi.fn(() =>
Promise.resolve({ data: { token: 'token' }, ok: true }),
)
const mockSuccessResendAccount = vi.fn(() => Promise.resolve({ ok: true }))
const onOkaySpy = vi.fn()
const email = '[email protected]'
const password = 'Password1'

afterEach(() => {
vi.restoreAllMocks()
vi.clearAllMocks()
})

describe('useResendAccountConfirmation', () => {
describe('when everything is successful', () => {
it('changes the password for the user', async () => {
vi.mock('lib/api/Account', () => ({
Account: function Account() {
this.loginCreate = mockSuccessLogin
this.confirmCreate = mockSuccessResendAccount
},
}))

await useLoginUser({ email, password }, { onOkay: onOkaySpy })
await useResendAccountConfirmation()

expect(mockSuccessResendAccount).toBeCalledTimes(1)
expect(mockSuccessResendAccount).toBeCalledWith({
headers: { Authorization: 'Bearer token' },
})
})
})
})
6 changes: 5 additions & 1 deletion composables/api/useValidateRecoveryToken/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ApiResponse, optionalParameters, useApi } from 'composables/useApi'
import {
type ApiResponse,
type optionalParameters,
useApi,
} from 'composables/useApi'
import { Account } from 'lib/api/Account'

export async function useValidateRecoveryToken(
Expand Down
3 changes: 3 additions & 0 deletions i18n/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import type { LanguageIndexFile } from '../language'

export const English = {
login: 'Log In',
logout: 'Log Out',
needToConfirm: 'Looks like you still need to confirm your email address.',
resendConfirmation: 'Resend confirmation',
signup: 'Sign Up',
welcome: 'Welcome',
} satisfies LanguageIndexFile
5 changes: 4 additions & 1 deletion i18n/language.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export interface LanguageIndexFile {
welcome: string
signup?: string
login?: string
logout?: string
needToConfirm?: string
resendConfirmation?: string
signup?: string
}
2 changes: 2 additions & 0 deletions layouts/default.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import BasicAlert from 'blocks/cards/BasicAlert/BasicAlert.vue'
import ResendConfirmationBar from 'blocks/ResendConfirmationBar/ResendConfirmationBar.vue'
import SiteFooter from 'blocks/SiteFooter/SiteFooter.vue'
import SiteNavbar from 'blocks/nav/SiteNavbar/SiteNavbar.vue'
</script>
Expand All @@ -8,6 +9,7 @@ import SiteNavbar from 'blocks/nav/SiteNavbar/SiteNavbar.vue'
<div class="mx-auto flex h-screen flex-col">
<BasicAlert />
<SiteNavbar />
<ResendConfirmationBar />
<slot />
<SiteFooter />
</div>
Expand Down
Loading