Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6555bfa
feat(device-token): send token to server
mrlnstk Feb 6, 2025
0afe0cb
refactor(nuxt-config): uncomment sentry
mrlnstk Feb 6, 2025
1fbff08
Merge branch 'master' into feat/implement-device-registration
mrlnstk Feb 6, 2025
b2e1354
Merge branch 'master' into feat/implement-device-registration
mrlnstk Feb 9, 2025
3d90a6b
feat(device-token): remove token on logout
mrlnstk Feb 9, 2025
8e6db6e
feat(notification): adjust notification api to accept userIds
mrlnstk Feb 10, 2025
9ea4882
Merge branch 'master' into feat/implement-device-registration
mrlnstk Feb 27, 2025
27da6f4
Merge remote-tracking branch 'origin/main' into feat/implement-device…
mrlnstk May 18, 2025
3b352df
fix(fcm-registration): move code to new page files
mrlnstk May 18, 2025
5c7621e
refactor(session): remove unused file
mrlnstk May 18, 2025
6d30831
fix(notification): move update token function
mrlnstk May 18, 2025
3750a97
fix(notification-store): fix import errors
mrlnstk May 18, 2025
9871d9b
feat(device-token): add mutations and queries
mrlnstk May 18, 2025
dea3e2c
fix(gql): fix generated code
mrlnstk May 19, 2025
3f474ad
fix(mutations): update device mutations
mrlnstk Jul 17, 2025
653282d
Merge branch 'main' into feat/implement-device-registration
mrlnstk Jul 17, 2025
43b22d7
feat(mutations): generate code files
mrlnstk Jul 17, 2025
bc23afb
fix(mutations): fix mutations
mrlnstk Jul 17, 2025
db6875e
feat(fcm-token): finalize fcm token registration
mrlnstk Jul 17, 2025
7116de7
fix(gql): regenereate gql files
mrlnstk Jul 17, 2025
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
19 changes: 17 additions & 2 deletions src/app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@
import '@fontsource-variable/raleway'
import { isEqual } from 'ufo'

const { $pwa } = useNuxtApp()
const { $urql, $pwa } = useNuxtApp()
const { isApp } = usePlatform()
const runtimeConfig = useRuntimeConfig()
const timeZone = useTimeZone()
const localePath = useLocalePath()
const store = useStore()
const notificationStore = useNotificationStore()
const route = useRoute()

// i18n
Expand All @@ -69,7 +70,17 @@ const { t, locale } = useI18n()
const loadingId = Math.random()
const loadingIds = useState(STATE_LOADING_IDS_NAME, () => [loadingId])
const isLoading = computed(() => !!loadingIds.value.length)
onMounted(() => loadingIds.value.splice(loadingIds.value.indexOf(loadingId), 1))

const handleVisibilityChange = async () => {
if (document.visibilityState == 'visible')
await notificationStore.updateRemoteFcmToken($urql.value, store)
}

onMounted(async () => {
loadingIds.value.splice(loadingIds.value.indexOf(loadingId), 1)
document.addEventListener('visibilitychange', handleVisibilityChange)
await notificationStore.updateRemoteFcmToken($urql.value, store)
})

// browserslist
const isBrowserSupported = ref(true)
Expand All @@ -78,6 +89,10 @@ onBeforeMount(async () => {
isBrowserSupported.value = supportedBrowsers.test(navigator.userAgent)
})

onBeforeUnmount(() => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
})

// methods
const initialize = async () => {
if (import.meta.client) {
Expand Down
6 changes: 6 additions & 0 deletions src/app/composables/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const useJwtRefresh = () => {
const jwtFromCookie = useJwtFromCookie()
const runtimeConfig = useRuntimeConfig()
const store = useStore()
const notificationStore = useNotificationStore()

return async () =>
await jwtRefresh({
Expand All @@ -54,6 +55,7 @@ export const useJwtRefresh = () => {
id: jwtFromCookie?.jwtDecoded.id as string,
runtimeConfig,
store,
notificationStore,
})
}

Expand All @@ -79,9 +81,13 @@ export const useSignOut = async () => {
const { $urql, $urqlReset, ssrContext } = useNuxtApp()
const store = useStore()
const runtimeConfig = useRuntimeConfig()
const notificationStore = useNotificationStore()

return {
async signOut() {
await notificationStore.updateRemoteFcmToken($urql.value, store, {
remove: true,
})
await signOut({
$urqlReset,
client: $urql.value,
Expand Down
4 changes: 4 additions & 0 deletions src/app/pages/session/create/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ const templateIdTitle = useId()
const localePath = useLocalePath()
const route = useRoute()
const store = useStore()
const notificationStore = useNotificationStore()

const { $urql } = useNuxtApp()

// stepper
const { error, restart, step, title } = useStepperPage<'default'>({
Expand All @@ -91,6 +94,7 @@ const to = computed(() =>
)
const verified = computed(() => route.query.verified === null)
const onSignIn = async () => {
await notificationStore.updateRemoteFcmToken($urql.value, store)
// A link that allows users to delete their account is required by the Google Play Store (https://support.google.com/googleplay/android-developer/answer/13316080#account_deletion)
// TODO: generalize, potentially whitelist valid redirection targets
if (to.value === 'account-deletion') {
Expand Down
2 changes: 2 additions & 0 deletions src/app/plugins/notification.client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export default defineNuxtPlugin(() => {
const notificationStore = useNotificationStore()

requestNotificationPermissionState(notificationStore)

if (hasPushCapability) {
registerIosCallbackHandler(notificationStore)
} else {
Expand Down
44 changes: 43 additions & 1 deletion src/app/stores/notification.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import type { Client } from '@urql/core'
import { createDeviceMutation } from '~~/gql/documents/mutations/device/deviceCreate'
import { deleteDeviceMutation } from '~~/gql/documents/mutations/device/deviceDelete'

export const useNotificationStore = defineStore('notification', () => {
const fcmToken = ref<string>()
const permissionState = ref<PermissionState>()

// Initializes the FCM token, should only be called after a user gave notification permission
// Main notification function that initializes the FCM token.
// It should only be called after a user gave notification permission.
// Token registration etc. is handled automatically.
const fcmTokenInitialize = async () => {
if (hasPushCapability) {
window.webkit?.messageHandlers['push-token']?.postMessage('push-token')
Expand All @@ -11,9 +17,45 @@ export const useNotificationStore = defineStore('notification', () => {
}
}

const updateRemoteFcmToken = async (
client: Client,
store: ReturnType<typeof useStore>,
options?: { remove: boolean },
) => {
if (permissionState.value !== 'granted' || !store.signedInAccountId) return

if (!fcmToken.value) fcmTokenInitialize()

const token = fcmToken.value

if (!token) return

if (options?.remove) {
await client
.mutation(deleteDeviceMutation, {
deleteDeviceInput: {
createdBy: store.signedInAccountId ?? '',
fcmToken: token,
},
})
.toPromise()
return
}

await client
.mutation(createDeviceMutation, {
deviceInput: {
createdBy: store.signedInAccountId ?? '',
fcmToken: token,
},
})
.toPromise()
}

return {
fcmToken,
fcmTokenInitialize,
permissionState,
updateRemoteFcmToken,
}
})
28 changes: 15 additions & 13 deletions src/app/utils/pwa/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,21 @@ export const requestNotificationPermission = (
}
}

// // Requests the current notification permission state
// export const requestNotificationPermissionState = async () => {
// if (hasPushCapability) {
// window.webkit?.messageHandlers['push-permission-state']?.postMessage(
// 'push-permission-state',
// )
// } else {
// const permissionStatus = await navigator.permissions.query({
// name: 'notifications',
// })
// permissionState.value = permissionStatus.state
// }
// }
// Requests the current notification permission state
export const requestNotificationPermissionState = async (
notificationStore: ReturnType<typeof useNotificationStore>,
) => {
if (hasPushCapability) {
window.webkit?.messageHandlers['push-permission-state']?.postMessage(
'push-permission-state',
)
} else {
const permissionStatus = await navigator.permissions.query({
name: 'notifications',
})
notificationStore.permissionState = permissionStatus.state
}
}

export const registerIosCallbackHandler = (
notificationStore: ReturnType<typeof useNotificationStore>,
Expand Down
12 changes: 12 additions & 0 deletions src/gql/documents/mutations/device/deviceCreate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useMutation } from '@urql/vue'
import { graphql } from '~~/gql/generated'

export const createDeviceMutation = graphql(`
mutation createDevice($deviceInput: DeviceInput!) {
createDevice(input: { device: $deviceInput }) {
clientMutationId
}
}
`)

export const useCreateDeviceMutation = () => useMutation(createDeviceMutation)
14 changes: 14 additions & 0 deletions src/gql/documents/mutations/device/deviceDelete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useMutation } from '@urql/vue'
import { graphql } from '~~/gql/generated'

export const deleteDeviceMutation = graphql(`
mutation deleteDeviceByCreatedByAndFcmToken(
$deleteDeviceInput: DeleteDeviceByCreatedByAndFcmTokenInput!
) {
deleteDeviceByCreatedByAndFcmToken(input: $deleteDeviceInput) {
clientMutationId
}
}
`)

export const useDeleteDeviceMutation = () => useMutation(deleteDeviceMutation)
14 changes: 14 additions & 0 deletions src/gql/documents/mutations/device/deviceUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useMutation } from '@urql/vue'
import { graphql } from '~~/gql/generated'

export const updateDeviceMutation = graphql(`
mutation updateDeviceByCreatedByAndFcmToken(
$updateDeviceInput: UpdateDeviceByCreatedByAndFcmTokenInput!
) {
updateDeviceByCreatedByAndFcmToken(input: $updateDeviceInput) {
clientMutationId
}
}
`)

export const useUpdateDeviceMutation = () => useMutation(updateDeviceMutation)
19 changes: 19 additions & 0 deletions src/gql/documents/queries/device/deviceByFcmToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useQuery } from '@urql/vue'
import { graphql } from '~~/gql/generated'
import type { DeviceByCreatedByAndFcmTokenQueryVariables } from '~~/gql/generated/graphql'

export const useDeviceByFcmTokenQuery = (
variables: DeviceByCreatedByAndFcmTokenQueryVariables,
) =>
useQuery({
query: deviceByCreatedByAndFcmTokenQuery,
variables,
})

export const deviceByCreatedByAndFcmTokenQuery = graphql(`
query deviceByCreatedByAndFcmToken($createdBy: UUID!, $fcmToken: String!) {
deviceByCreatedByAndFcmToken(createdBy: $createdBy, fcmToken: $fcmToken) {
id
}
}
`)
36 changes: 36 additions & 0 deletions src/gql/generated/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ type Documents = {
'\n mutation CreateContact($contactInput: ContactInput!) {\n createContact(input: { contact: $contactInput }) {\n contact {\n ...ContactItem\n }\n }\n }\n': typeof types.CreateContactDocument
'\n mutation DeleteContactById($id: UUID!) {\n deleteContactById(input: { id: $id }) {\n clientMutationId\n contact {\n ...ContactItem\n }\n }\n }\n': typeof types.DeleteContactByIdDocument
'\n mutation UpdateContactById($id: UUID!, $contactPatch: ContactPatch!) {\n updateContactById(input: { id: $id, contactPatch: $contactPatch }) {\n contact {\n ...ContactItem\n }\n }\n }\n': typeof types.UpdateContactByIdDocument
'\n mutation createDevice($deviceInput: DeviceInput!) {\n createDevice(input: { device: $deviceInput }) {\n clientMutationId\n }\n }\n': typeof types.CreateDeviceDocument
'\n mutation deleteDeviceByCreatedByAndFcmToken(\n $deleteDeviceInput: DeleteDeviceByCreatedByAndFcmTokenInput!\n ) {\n deleteDeviceByCreatedByAndFcmToken(input: $deleteDeviceInput) {\n clientMutationId\n }\n }\n': typeof types.DeleteDeviceByCreatedByAndFcmTokenDocument
'\n mutation updateDeviceByCreatedByAndFcmToken(\n $updateDeviceInput: UpdateDeviceByCreatedByAndFcmTokenInput!\n ) {\n updateDeviceByCreatedByAndFcmToken(input: $updateDeviceInput) {\n clientMutationId\n }\n }\n': typeof types.UpdateDeviceByCreatedByAndFcmTokenDocument
'\n mutation CreateEvent($input: EventInput!) {\n createEvent(input: { event: $input }) {\n event {\n ...EventItem\n }\n }\n }\n': typeof types.CreateEventDocument
'\n mutation EventDelete($id: UUID!, $password: String!) {\n eventDelete(input: { id: $id, password: $password }) {\n clientMutationId\n event {\n ...EventItem\n }\n }\n }\n': typeof types.EventDeleteDocument
'\n mutation EventUnlock($guestId: UUID!) {\n eventUnlock(input: { guestId: $guestId }) {\n eventUnlockResponse {\n creatorUsername\n eventSlug\n jwt\n }\n }\n }\n': typeof types.EventUnlockDocument
Expand All @@ -83,6 +86,7 @@ type Documents = {
'\n query AccountUploadQuotaBytes {\n accountUploadQuotaBytes\n }\n': typeof types.AccountUploadQuotaBytesDocument
'\n query AllAchievements($accountId: UUID) {\n allAchievements(condition: { accountId: $accountId }) {\n nodes {\n ...AchievementItem\n }\n }\n }\n': typeof types.AllAchievementsDocument
'\n query AllContacts($after: Cursor, $createdBy: UUID, $first: Int!) {\n allContacts(\n after: $after\n condition: { createdBy: $createdBy }\n first: $first\n orderBy: [FIRST_NAME_ASC, LAST_NAME_ASC]\n ) {\n nodes {\n ...ContactItem\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n }\n }\n': typeof types.AllContactsDocument
'\n query deviceByCreatedByAndFcmToken($createdBy: UUID!, $fcmToken: String!) {\n deviceByCreatedByAndFcmToken(createdBy: $createdBy, fcmToken: $fcmToken) {\n id\n }\n }\n': typeof types.DeviceByCreatedByAndFcmTokenDocument
'\n query EventByCreatedByAndSlug(\n $createdBy: UUID!\n $guestId: UUID\n $slug: String!\n ) {\n eventByCreatedByAndSlug(createdBy: $createdBy, slug: $slug) {\n ...EventItem\n guestsByEventId(condition: { id: $guestId }) {\n nodes {\n ...GuestItem\n contactByContactId {\n ...ContactItem\n }\n }\n }\n }\n }\n': typeof types.EventByCreatedByAndSlugDocument
'\n query AllEventCategories {\n allEventCategories {\n nodes {\n ...EventCategoryItem\n }\n }\n }\n': typeof types.AllEventCategoriesDocument
'\n query AllEventFormats {\n allEventFormats {\n nodes {\n ...EventFormatItem\n }\n }\n }\n': typeof types.AllEventFormatsDocument
Expand Down Expand Up @@ -189,6 +193,12 @@ const documents: Documents = {
types.DeleteContactByIdDocument,
'\n mutation UpdateContactById($id: UUID!, $contactPatch: ContactPatch!) {\n updateContactById(input: { id: $id, contactPatch: $contactPatch }) {\n contact {\n ...ContactItem\n }\n }\n }\n':
types.UpdateContactByIdDocument,
'\n mutation createDevice($deviceInput: DeviceInput!) {\n createDevice(input: { device: $deviceInput }) {\n clientMutationId\n }\n }\n':
types.CreateDeviceDocument,
'\n mutation deleteDeviceByCreatedByAndFcmToken(\n $deleteDeviceInput: DeleteDeviceByCreatedByAndFcmTokenInput!\n ) {\n deleteDeviceByCreatedByAndFcmToken(input: $deleteDeviceInput) {\n clientMutationId\n }\n }\n':
types.DeleteDeviceByCreatedByAndFcmTokenDocument,
'\n mutation updateDeviceByCreatedByAndFcmToken(\n $updateDeviceInput: UpdateDeviceByCreatedByAndFcmTokenInput!\n ) {\n updateDeviceByCreatedByAndFcmToken(input: $updateDeviceInput) {\n clientMutationId\n }\n }\n':
types.UpdateDeviceByCreatedByAndFcmTokenDocument,
'\n mutation CreateEvent($input: EventInput!) {\n createEvent(input: { event: $input }) {\n event {\n ...EventItem\n }\n }\n }\n':
types.CreateEventDocument,
'\n mutation EventDelete($id: UUID!, $password: String!) {\n eventDelete(input: { id: $id, password: $password }) {\n clientMutationId\n event {\n ...EventItem\n }\n }\n }\n':
Expand Down Expand Up @@ -235,6 +245,8 @@ const documents: Documents = {
types.AllAchievementsDocument,
'\n query AllContacts($after: Cursor, $createdBy: UUID, $first: Int!) {\n allContacts(\n after: $after\n condition: { createdBy: $createdBy }\n first: $first\n orderBy: [FIRST_NAME_ASC, LAST_NAME_ASC]\n ) {\n nodes {\n ...ContactItem\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n }\n }\n':
types.AllContactsDocument,
'\n query deviceByCreatedByAndFcmToken($createdBy: UUID!, $fcmToken: String!) {\n deviceByCreatedByAndFcmToken(createdBy: $createdBy, fcmToken: $fcmToken) {\n id\n }\n }\n':
types.DeviceByCreatedByAndFcmTokenDocument,
'\n query EventByCreatedByAndSlug(\n $createdBy: UUID!\n $guestId: UUID\n $slug: String!\n ) {\n eventByCreatedByAndSlug(createdBy: $createdBy, slug: $slug) {\n ...EventItem\n guestsByEventId(condition: { id: $guestId }) {\n nodes {\n ...GuestItem\n contactByContactId {\n ...ContactItem\n }\n }\n }\n }\n }\n':
types.EventByCreatedByAndSlugDocument,
'\n query AllEventCategories {\n allEventCategories {\n nodes {\n ...EventCategoryItem\n }\n }\n }\n':
Expand Down Expand Up @@ -551,6 +563,24 @@ export function graphql(
export function graphql(
source: '\n mutation UpdateContactById($id: UUID!, $contactPatch: ContactPatch!) {\n updateContactById(input: { id: $id, contactPatch: $contactPatch }) {\n contact {\n ...ContactItem\n }\n }\n }\n',
): (typeof documents)['\n mutation UpdateContactById($id: UUID!, $contactPatch: ContactPatch!) {\n updateContactById(input: { id: $id, contactPatch: $contactPatch }) {\n contact {\n ...ContactItem\n }\n }\n }\n']
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: '\n mutation createDevice($deviceInput: DeviceInput!) {\n createDevice(input: { device: $deviceInput }) {\n clientMutationId\n }\n }\n',
): (typeof documents)['\n mutation createDevice($deviceInput: DeviceInput!) {\n createDevice(input: { device: $deviceInput }) {\n clientMutationId\n }\n }\n']
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: '\n mutation deleteDeviceByCreatedByAndFcmToken(\n $deleteDeviceInput: DeleteDeviceByCreatedByAndFcmTokenInput!\n ) {\n deleteDeviceByCreatedByAndFcmToken(input: $deleteDeviceInput) {\n clientMutationId\n }\n }\n',
): (typeof documents)['\n mutation deleteDeviceByCreatedByAndFcmToken(\n $deleteDeviceInput: DeleteDeviceByCreatedByAndFcmTokenInput!\n ) {\n deleteDeviceByCreatedByAndFcmToken(input: $deleteDeviceInput) {\n clientMutationId\n }\n }\n']
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: '\n mutation updateDeviceByCreatedByAndFcmToken(\n $updateDeviceInput: UpdateDeviceByCreatedByAndFcmTokenInput!\n ) {\n updateDeviceByCreatedByAndFcmToken(input: $updateDeviceInput) {\n clientMutationId\n }\n }\n',
): (typeof documents)['\n mutation updateDeviceByCreatedByAndFcmToken(\n $updateDeviceInput: UpdateDeviceByCreatedByAndFcmTokenInput!\n ) {\n updateDeviceByCreatedByAndFcmToken(input: $updateDeviceInput) {\n clientMutationId\n }\n }\n']
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down Expand Up @@ -689,6 +719,12 @@ export function graphql(
export function graphql(
source: '\n query AllContacts($after: Cursor, $createdBy: UUID, $first: Int!) {\n allContacts(\n after: $after\n condition: { createdBy: $createdBy }\n first: $first\n orderBy: [FIRST_NAME_ASC, LAST_NAME_ASC]\n ) {\n nodes {\n ...ContactItem\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n }\n }\n',
): (typeof documents)['\n query AllContacts($after: Cursor, $createdBy: UUID, $first: Int!) {\n allContacts(\n after: $after\n condition: { createdBy: $createdBy }\n first: $first\n orderBy: [FIRST_NAME_ASC, LAST_NAME_ASC]\n ) {\n nodes {\n ...ContactItem\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n }\n }\n']
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: '\n query deviceByCreatedByAndFcmToken($createdBy: UUID!, $fcmToken: String!) {\n deviceByCreatedByAndFcmToken(createdBy: $createdBy, fcmToken: $fcmToken) {\n id\n }\n }\n',
): (typeof documents)['\n query deviceByCreatedByAndFcmToken($createdBy: UUID!, $fcmToken: String!) {\n deviceByCreatedByAndFcmToken(createdBy: $createdBy, fcmToken: $fcmToken) {\n id\n }\n }\n']
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading
Loading