Skip to content

Commit

Permalink
feat(fe): auto accept stream/project invite on email link click [WBX-…
Browse files Browse the repository at this point in the history
…73] (specklesystems#2017)

* feat(fe2): project invite auto accept

* fix(fe2): improved CSR error logging

* feat(fe1): auto accept stream invite on email link click

* minor type fix

* tests fix
  • Loading branch information
fabis94 authored Feb 7, 2024
1 parent 53ca361 commit 9caa2a3
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 16 deletions.
33 changes: 28 additions & 5 deletions packages/frontend-2/components/projects/invite/Banner.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<template>
<div
v-if="invite"
class="flex flex-col space-y-4 sm:space-y-0 sm:space-x-2 sm:items-center sm:flex-row px-4 py-5 sm:py-2 transition hover:bg-primary-muted"
>
<div class="flex space-x-2 items-center grow text-sm">
Expand Down Expand Up @@ -32,6 +33,7 @@
</template>
</div>
</div>
<div v-else />
</template>
<script setup lang="ts">
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
Expand All @@ -43,6 +45,7 @@ import { usePostAuthRedirect } from '~~/lib/auth/composables/postAuthRedirect'
import type { Optional } from '@speckle/shared'
import { CheckIcon } from '@heroicons/vue/24/solid'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { ToastNotificationType, useGlobalToast } from '~/lib/common/composables/toast'
graphql(`
fragment ProjectsInviteBanner on PendingStreamCollaborator {
Expand All @@ -62,8 +65,9 @@ const emit = defineEmits<{
const props = withDefaults(
defineProps<{
invite: ProjectsInviteBannerFragment
invite?: ProjectsInviteBannerFragment
showStreamName?: boolean
autoAccept?: boolean
}>(),
{ showStreamName: true }
)
Expand All @@ -73,15 +77,16 @@ const { isLoggedIn } = useActiveUser()
const processInvite = useProcessProjectInvite()
const postAuthRedirect = usePostAuthRedirect()
const goToLogin = useNavigateToLogin()
const { triggerNotification } = useGlobalToast()
const loading = ref(false)
const mp = useMixpanel()
const token = computed(
() => props.invite.token || (route.query.token as Optional<string>)
() => props.invite?.token || (route.query.token as Optional<string>)
)
const useInvite = async (accept: boolean) => {
if (!token.value) return
if (!token.value || !props.invite) return
loading.value = true
const success = await processInvite(
Expand All @@ -94,8 +99,14 @@ const useInvite = async (accept: boolean) => {
)
loading.value = false
if (success) {
emit('processed', { accepted: accept })
if (!success) return
emit('processed', { accepted: accept })
if (accept) {
triggerNotification({
type: ToastNotificationType.Success,
title: "You've joined the project!"
})
}
mp.track('Invite Action', {
Expand All @@ -112,4 +123,16 @@ const onLoginClick = () => {
}
})
}
if (process.client) {
watch(
() => props.autoAccept,
async (newVal, oldVal) => {
if (newVal && !oldVal) {
await useInvite(true)
}
},
{ immediate: true }
)
}
</script>
7 changes: 5 additions & 2 deletions packages/frontend-2/lib/core/configs/apollo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { onError } from '@apollo/client/link/error'
import { useNavigateToLogin, loginRoute } from '~~/lib/common/helpers/route'
import { useAppErrorState } from '~~/lib/core/composables/appErrorState'
import { isInvalidAuth } from '~~/lib/common/helpers/graphql'
import { omit } from 'lodash-es'
import { isBoolean, omit } from 'lodash-es'
import { useRequestId } from '~/lib/core/composables/server'

const appName = 'frontend-2'
Expand Down Expand Up @@ -302,7 +302,10 @@ function createLink(params: {
'need a token to subscribe'
)

const shouldSkip = !!res.operation.getContext().skipLoggingErrors
const skipLoggingErrors = res.operation.getContext().skipLoggingErrors
const shouldSkip = isBoolean(skipLoggingErrors)
? skipLoggingErrors
: skipLoggingErrors?.(res)
if (!isSubTokenMissingError && !shouldSkip) {
const errMsg = res.networkError?.message || res.graphQLErrors?.[0]?.message
logger.error(
Expand Down
30 changes: 26 additions & 4 deletions packages/frontend-2/pages/projects/[id]/index.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
<template>
<div>
<template v-if="project">
<ProjectsInviteBanner v-if="invite" :invite="invite" :show-stream-name="false" />
<ProjectsInviteBanner
:invite="invite"
:show-stream-name="false"
:auto-accept="shouldAutoAcceptInvite"
@processed="onInviteAccepted"
/>
<!-- Heading text w/ actions -->
<ProjectPageHeader :project="project" />
<!-- Stats blocks -->
Expand Down Expand Up @@ -60,28 +65,45 @@ definePageMeta({
})
const route = useRoute()
const router = useRouter()
const projectId = computed(() => route.params.id as string)
const shouldAutoAcceptInvite = computed(() => route.query.accept === 'true')
const token = computed(() => route.query.token as Optional<string>)
useGeneralProjectPageUpdateTracking({ projectId }, { notifyOnProjectUpdate: true })
const { result: projectPageResult } = useQuery(
projectPageQuery,
() => ({
id: projectId.value,
token: (route.query.token as Optional<string>) || null
token: token.value
}),
() => ({
// Custom error policy so that a failing invitedTeam resolver (due to access rights)
// doesn't kill the entire query
errorPolicy: 'all'
errorPolicy: 'all',
context: {
skipLoggingErrors: (err) =>
err.graphQLErrors?.length === 1 &&
err.graphQLErrors.some((e) => !!e.path?.includes('invitedTeam'))
}
})
)
const project = computed(() => projectPageResult.value?.project)
const invite = computed(() => projectPageResult.value?.projectInvite)
const invite = computed(() => projectPageResult.value?.projectInvite || undefined)
const projectName = computed(() =>
project.value?.name.length ? project.value.name : ''
)
useHead({
title: projectName
})
const onInviteAccepted = async (params: { accepted: boolean }) => {
if (params.accepted) {
await router.replace({
query: { ...route.query, accept: undefined, token: undefined }
})
}
}
</script>
14 changes: 11 additions & 3 deletions packages/frontend-2/plugins/001-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,21 @@ export default defineNuxtPlugin(async (nuxtApp) => {
otherData.unshift(firstString)
}

const otherDataObjects = otherData.filter(isObjectLike)
const otherDataNonObjects = otherData.filter((o) => !isObjectLike(o))
const mergedOtherDataObject = Object.assign({}, ...otherDataObjects) as Record<
string,
unknown
>

seqLogger.emit({
timestamp: new Date(),
level: 'error',
messageTemplate: 'Client-side error: {errorMessage}',
messageTemplate: 'Client-side error: {mainSeqErrorMessage}',
properties: {
errorMessage,
extraData: otherData,
mainSeqErrorMessage: errorMessage, // weird name to avoid collision with otherData
extraData: otherDataNonObjects,
...mergedOtherDataObject,
...collectCoreInfo()
},
exception
Expand Down
12 changes: 12 additions & 0 deletions packages/frontend-2/type-augmentations/apollo.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
declare module '@apollo/client/core' {
interface DefaultContext {
/**
* Whether to skip logging errors caused in this operation
*/
skipLoggingErrors?:
| boolean
| ((err: import('@apollo/client/link/error').ErrorResponse) => boolean)
}
}

export {}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export const UsersStreamInviteMixin = vueWithMixins(IsLoggedInMixin).extend({
inviteToken: {
type: String as PropType<Nullable<string>>,
default: null
},
autoAccept: {
type: Boolean,
default: false
}
},
data: () => ({
Expand Down Expand Up @@ -167,5 +171,10 @@ export const UsersStreamInviteMixin = vueWithMixins(IsLoggedInMixin).extend({
})
}
}
},
mounted() {
if (this.autoAccept) {
this.acceptInvite()
}
}
})
5 changes: 5 additions & 0 deletions packages/frontend/src/main/pages/stream/TheStream.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
v-if="hasInvite && !showInvitePlaceholder"
:stream-invite="streamInvite"
:invite-token="inviteToken"
:auto-accept="shouldAutoAcceptInvite"
@invite-used="onInviteClosed"
/>

Expand All @@ -26,6 +27,7 @@
v-if="showInvitePlaceholder"
:stream-invite="streamInvite"
:invite-token="inviteToken"
:auto-accept="shouldAutoAcceptInvite"
@invite-used="onInviteClosed"
/>
<error-placeholder v-else :error-type="errorType">
Expand Down Expand Up @@ -151,6 +153,9 @@ export default defineComponent({
inviteToken(): Nullable<string> {
return getInviteTokenFromRoute(this.$route)
},
shouldAutoAcceptInvite(): boolean {
return this.$route.query.accept === 'true'
},
errorMsg(): MaybeFalsy<string> {
return this.error?.message.replace('GraphQL error: ', '')
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ function buildInviteLink(invite) {

if (resourceTarget === 'streams') {
return new URL(
`${getStreamRoute(resourceId)}?token=${token}`,
`${getStreamRoute(resourceId)}?token=${token}&accept=true`,
getFrontendOrigin()
).toString()
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async function cleanup() {

function getInviteTokenFromEmailParams(emailParams) {
const { text } = emailParams
const [, inviteId] = text.match(/\?token=(.*)\s/i)
const [, inviteId] = text.match(/\?token=(.*?)(\s|&)/i)
return inviteId
}

Expand Down

0 comments on commit 9caa2a3

Please sign in to comment.