Skip to content

Commit

Permalink
Merge pull request #7422 from opengovsg/refactor/generalize-seen-flag
Browse files Browse the repository at this point in the history
refactor: generalize user seen flag
  • Loading branch information
KenLSM authored Jul 4, 2024
2 parents 81495c1 + d883952 commit 2740ece
Show file tree
Hide file tree
Showing 18 changed files with 206 additions and 99 deletions.
29 changes: 16 additions & 13 deletions frontend/src/app/AdminNavBar/AdminNavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
useDisclosure,
} from '@chakra-ui/react'

import { SeenFlags } from '~shared/types'

import { BxsHelpCircle } from '~assets/icons/BxsHelpCircle'
import { BxsRocket } from '~assets/icons/BxsRocket'
import { ReactComponent as BrandMarkSvg } from '~assets/svgs/brand/brand-mark-colour.svg'
Expand All @@ -31,12 +33,12 @@ import IconButton from '~components/IconButton'
import Link from '~components/Link'
import { AvatarMenu, AvatarMenuDivider } from '~templates/AvatarMenu/AvatarMenu'

import { SeenFlagsMapVersion } from '~features/user/constants'
import { EmergencyContactModal } from '~features/user/emergency-contact/EmergencyContactModal'
import { useUserMutations } from '~features/user/mutations'
import { useUser } from '~features/user/queries'
import { TransferOwnershipModal } from '~features/user/transfer-ownership/TransferOwnershipModal'
import { FEATURE_UPDATE_LIST } from '~features/whats-new/FeatureUpdateList'
import { getShowLatestFeatureUpdateNotification } from '~features/whats-new/utils/utils'
import { getShowFeatureFlagLastSeen } from '~features/user/utils'
import { WhatsNewDrawer } from '~features/whats-new/WhatsNewDrawer'

import Menu from '../../components/Menu'
Expand Down Expand Up @@ -156,7 +158,7 @@ export interface AdminNavBarProps {

export const AdminNavBar = ({ isMenuOpen }: AdminNavBarProps): JSX.Element => {
const { user, isLoading: isUserLoading, removeQuery } = useUser()
const { updateLastSeenFeatureVersionMutation } = useUserMutations()
const { updateLastSeenFlagMutation } = useUserMutations()

const whatsNewFeatureDrawerDisclosure = useDisclosure()

Expand Down Expand Up @@ -199,26 +201,27 @@ export const AdminNavBar = ({ isMenuOpen }: AdminNavBarProps): JSX.Element => {

const shouldShowFeatureUpdateNotification = useMemo(() => {
if (isUserLoading || !user) return false
return getShowLatestFeatureUpdateNotification(user)
return getShowFeatureFlagLastSeen(
user,
SeenFlags.LastSeenFeatureUpdateVersion,
)
}, [isUserLoading, user])

const onWhatsNewDrawerOpen = useCallback(() => {
if (isUserLoading || !user) return
// Update version if current user version is not set or is less than the latest version.
if (
user.flags?.lastSeenFeatureUpdateVersion === undefined ||
user.flags?.lastSeenFeatureUpdateVersion < FEATURE_UPDATE_LIST.version
) {
updateLastSeenFeatureVersionMutation.mutateAsync(
FEATURE_UPDATE_LIST.version,
)
if (shouldShowFeatureUpdateNotification) {
updateLastSeenFlagMutation.mutateAsync({
version: SeenFlagsMapVersion.lastSeenFeatureUpdateVersion,
flag: SeenFlags.LastSeenFeatureUpdateVersion,
})
}
whatsNewFeatureDrawerDisclosure.onOpen()
}, [
isUserLoading,
updateLastSeenFeatureVersionMutation,
updateLastSeenFlagMutation,
user,
whatsNewFeatureDrawerDisclosure,
shouldShowFeatureUpdateNotification,
])

// Emergency contact modal appears after the rollout announcement modal
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiService } from './ApiService'
import { ApiService } from '../../services/ApiService'

export const getEnabledFeatureFlags = async (): Promise<Set<string>> => {
return ApiService.get<string[]>('/feature-flags/enabled').then(
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/features/feature-flags/queries.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useQuery, UseQueryResult } from 'react-query'

import { getEnabledFeatureFlags } from '~services/FeatureFlagService'
import { getEnabledFeatureFlags } from '~features/feature-flags/FeatureFlagService'

export const featureFlagsKeys = {
base: ['feature-flags'] as const,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {
SendUserContactOtpDto,
TransferOwnershipRequestDto,
UpdateUserLastSeenFlagDto,
UserDto,
VerifyUserContactOtpDto,
} from '~shared/types/user'

import { ApiService } from './ApiService'
import { ApiService } from '../../services/ApiService'

const ADMIN_FORM_ENDPOINT = '/admin/forms'
const USER_ENDPOINT = '/user'
Expand Down Expand Up @@ -34,12 +35,12 @@ export const verifyUserContactOtp = (
).then(({ data }) => data)
}

export const updateUserLastSeenFeatureUpdateVersion = async (
version: number,
export const updateUserLastSeenFlagVersion = async (
params: UpdateUserLastSeenFlagDto,
): Promise<UserDto> => {
return ApiService.post<UserDto>(
`${USER_ENDPOINT}/flag/new-features-last-seen`,
{ version },
`${USER_ENDPOINT}/flag/last-seen`,
params,
).then(({ data }) => data)
}

Expand Down
13 changes: 13 additions & 0 deletions frontend/src/features/user/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { SeenFlags } from '~shared/types'

import { FEATURE_UPDATE_LIST } from '~features/whats-new/FeatureUpdateList'

const LegacySeenFlags = {
[SeenFlags.LastSeenFeatureUpdateVersion]: FEATURE_UPDATE_LIST.version,
}

export const SeenFlagsMapVersion: { [key in SeenFlags]: number } = {
...LegacySeenFlags,
[SeenFlags.SettingsEmailNotification]: 0, // stub
[SeenFlags.CreateBuilderMrfWorkflow]: 0, // stub
}
14 changes: 8 additions & 6 deletions frontend/src/features/user/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@ import { useMutation, useQueryClient } from 'react-query'
import {
SendUserContactOtpDto,
TransferOwnershipRequestDto,
UpdateUserLastSeenFlagDto,
UserDto,
VerifyUserContactOtpDto,
} from '~shared/types/user'

import { ApiError } from '~typings/core'

import { useToast } from '~hooks/useToast'

import {
generateUserContactOtp,
transferOwnership,
updateUserLastSeenFeatureUpdateVersion,
updateUserLastSeenFlagVersion,
verifyUserContactOtp,
} from '~services/UserService'
} from '~features/user/UserService'

import { userKeys } from './queries'

Expand Down Expand Up @@ -43,11 +45,11 @@ export const useUserMutations = () => {
},
})

const updateLastSeenFeatureVersionMutation = useMutation<
const updateLastSeenFlagMutation = useMutation<
UserDto,
ApiError,
number
>((version: number) => updateUserLastSeenFeatureUpdateVersion(version), {
UpdateUserLastSeenFlagDto
>((params) => updateUserLastSeenFlagVersion(params), {
onSuccess: (newData) => {
queryClient.setQueryData(userKeys.base, newData)
},
Expand All @@ -72,7 +74,7 @@ export const useUserMutations = () => {
return {
generateOtpMutation,
verifyOtpMutation,
updateLastSeenFeatureVersionMutation,
updateLastSeenFlagMutation,
transferOwnershipMutation,
}
}
3 changes: 2 additions & 1 deletion frontend/src/features/user/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { UserDto } from '~shared/types/user'
import { LOGGED_IN_KEY } from '~constants/localStorage'
import { useLocalStorage } from '~hooks/useLocalStorage'
import { HttpError } from '~services/ApiService'
import { fetchUser } from '~services/UserService'

import { fetchUser } from '~features/user/UserService'

export const userKeys = {
base: ['user'] as const,
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/features/user/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { SeenFlags, UserDto } from '~shared/types'

import { SeenFlagsMapVersion } from './constants'

/**
* Returns whether the user should see the feature flag.
* @param user The user to check.
* @param flag The flag to check.
* @returns Boolean indicating whether the user should see the flag.
*/

export const getShowFeatureFlagLastSeen = (
user: UserDto | undefined,
flag: SeenFlags,
): boolean => {
const since = SeenFlagsMapVersion[flag]
const flagValue = user?.flags?.[flag]
if (flagValue == null) {
// If the flag is not set, failover as user has seen the flag.
return true
}

return flagValue < since
}
13 changes: 0 additions & 13 deletions frontend/src/features/whats-new/utils/utils.ts

This file was deleted.

17 changes: 12 additions & 5 deletions shared/types/user.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { z } from 'zod'
import type { Opaque } from 'type-fest'
import type { Tagged } from 'type-fest'

import { DateString } from './generic'
import { AgencyBase, AgencyDto, PublicAgencyDto } from './agency'
export type UserId = Tagged<string, 'UserId'>

export type UserId = Opaque<string, 'UserId'>
export enum SeenFlags {
LastSeenFeatureUpdateVersion = 'lastSeenFeatureUpdateVersion',
SettingsEmailNotification = 'settingsEmailNotification',
CreateBuilderMrfWorkflow = 'createBuilderMrfWorkflow',
}

// Base used for being referenced by schema/model in the backend.
// Note the lack of typing of _id.
Expand All @@ -18,9 +23,7 @@ export const UserBase = z.object({
postmanSms: z.boolean().optional(),
})
.optional(),
flags: z
.object({ lastSeenFeatureUpdateVersion: z.number().optional() })
.optional(),
flags: z.map(z.nativeEnum(SeenFlags), z.number()).optional(),
created: z.date(),
lastAccessed: z.date().optional(),
updatedAt: z.date(),
Expand Down Expand Up @@ -82,3 +85,7 @@ export type TransferOwnershipResponseDto = {
formIds: string[]
error: string
}
export type UpdateUserLastSeenFlagDto = {
version: number
flag: SeenFlags
}
3 changes: 2 additions & 1 deletion src/app/models/user.server.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ const compileUserModel = (db: Mongoose) => {
postmanSms: Boolean,
},
flags: {
lastSeenFeatureUpdateVersion: Number,
type: Schema.Types.Map, // of SeenFlags
of: Number,
},
apiToken: {
select: false,
Expand Down
Loading

0 comments on commit 2740ece

Please sign in to comment.