From f677f1167f628aaedae7abd4374b47ccd771cee3 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 28 Mar 2024 15:27:37 +0800 Subject: [PATCH] get feature list on web --- packages/api/src/generated/graphql.ts | 4 +-- packages/api/src/generated/schema.graphql | 2 +- .../api/src/resolvers/function_resolvers.ts | 25 ++++++++++--------- packages/api/src/schema.ts | 2 +- packages/api/src/services/features.ts | 6 ++--- packages/web/lib/featureFlag.ts | 7 +++++- .../networking/fragments/featureFragment.ts | 21 ++++++++++++++++ .../mutations/optIntoFeatureMutation.ts | 16 ++++++------ .../networking/queries/useGetViewerQuery.tsx | 6 +++++ packages/web/pages/settings/features/beta.tsx | 7 +++--- 10 files changed, 62 insertions(+), 34 deletions(-) create mode 100644 packages/web/lib/networking/fragments/featureFragment.ts diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 82b4cef2c..ae5bcdbd8 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -3696,7 +3696,7 @@ export enum UploadImportFileType { export type User = { __typename?: 'User'; email?: Maybe; - featureList?: Maybe>>; + featureList?: Maybe>; features?: Maybe>>; followersCount?: Maybe; friendsCount?: Maybe; @@ -7140,7 +7140,7 @@ export type UploadImportFileSuccessResolvers = { email?: Resolver, ParentType, ContextType>; - featureList?: Resolver>>, ParentType, ContextType>; + featureList?: Resolver>, ParentType, ContextType>; features?: Resolver>>, ParentType, ContextType>; followersCount?: Resolver, ParentType, ContextType>; friendsCount?: Resolver, ParentType, ContextType>; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 11bca2d29..94695e350 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -2984,7 +2984,7 @@ enum UploadImportFileType { type User { email: String - featureList: [Feature] + featureList: [Feature!] features: [String] followersCount: Int friendsCount: Int diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index cf391b6db..9a8fcfa58 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -368,6 +368,17 @@ export const functionResolvers = { } return undefined }, + async featureList( + _: User, + __: Record, + ctx: WithDataSourcesContext + ) { + if (!ctx.claims?.uid) { + return undefined + } + + return findUserFeatures(ctx.claims.uid) + }, async features( user: User, __: Record, @@ -376,18 +387,8 @@ export const functionResolvers = { if (!ctx.claims?.uid) { return undefined } - const userFeatures = await findUserFeatures(ctx.claims.uid) - return userFeatures.map((feature) => feature.name) - }, - async featureList( - _: User, - __: Record, - ctx: WithDataSourcesContext - ) { - if (!ctx.uid) { - return undefined - } - return findUserFeatures(ctx.uid) + + return (await findUserFeatures(ctx.claims.uid)).map((f) => f.name) }, }, Article: { diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 2e75d0ee6..e106514e4 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -89,7 +89,7 @@ const schema = gql` source: String intercomHash: String features: [String] - featureList: [Feature] + featureList: [Feature!] } type Profile { diff --git a/packages/api/src/services/features.ts b/packages/api/src/services/features.ts index 5c1172d27..664461a50 100644 --- a/packages/api/src/services/features.ts +++ b/packages/api/src/services/features.ts @@ -123,10 +123,8 @@ export const signFeatureToken = ( } export const findUserFeatures = async (userId: string) => { - return getRepository(Feature).find({ - where: { - user: { id: userId }, - }, + return getRepository(Feature).findBy({ + user: { id: userId }, }) } diff --git a/packages/web/lib/featureFlag.ts b/packages/web/lib/featureFlag.ts index f79369b78..4cae69bf6 100644 --- a/packages/web/lib/featureFlag.ts +++ b/packages/web/lib/featureFlag.ts @@ -7,5 +7,10 @@ export const userHasFeature = ( if (!user) { return false } - return user.features.includes(feature) + return user.featureList.some( + (f) => + f.name === feature && + f.grantedAt && + (!f.expiresAt || new Date(f.expiresAt) > new Date()) + ) } diff --git a/packages/web/lib/networking/fragments/featureFragment.ts b/packages/web/lib/networking/fragments/featureFragment.ts new file mode 100644 index 000000000..78dea92f4 --- /dev/null +++ b/packages/web/lib/networking/fragments/featureFragment.ts @@ -0,0 +1,21 @@ +import { gql } from 'graphql-request' + +export const featureFragment = gql` + fragment FeatureFields on Feature { + id + name + createdAt + updatedAt + grantedAt + expiresAt + } +` + +export interface Feature { + id: string + name: string + createdAt: Date + updatedAt?: Date + grantedAt?: Date + expiresAt?: Date +} diff --git a/packages/web/lib/networking/mutations/optIntoFeatureMutation.ts b/packages/web/lib/networking/mutations/optIntoFeatureMutation.ts index 26ac49d47..92b5f3b96 100644 --- a/packages/web/lib/networking/mutations/optIntoFeatureMutation.ts +++ b/packages/web/lib/networking/mutations/optIntoFeatureMutation.ts @@ -1,16 +1,17 @@ import { gql } from 'graphql-request' +import { Feature, featureFragment } from '../fragments/featureFragment' import { gqlFetcher } from '../networkHelpers' export interface OptInFeatureInput { name: string } -export interface OptInFeatureSuccess { - feature: { id: string } +export interface OptInFeatureResponse { + feature?: Feature } interface Response { - optInFeature: OptInFeatureSuccess + optInFeature: OptInFeatureResponse } export async function optInFeature( @@ -21,7 +22,7 @@ export async function optInFeature( optInFeature(input: $input) { ... on OptInFeatureSuccess { feature { - id + ...FeatureFields } } ... on OptInFeatureError { @@ -29,17 +30,14 @@ export async function optInFeature( } } } + ${featureFragment} ` try { const data = await gqlFetcher(mutation, { input, }) const output = data as Response | undefined - if ( - !output || - !output.optInFeature || - 'errorCodes' in output?.optInFeature - ) { + if (!output || !output.optInFeature.feature) { return false } return true diff --git a/packages/web/lib/networking/queries/useGetViewerQuery.tsx b/packages/web/lib/networking/queries/useGetViewerQuery.tsx index abf88c1ff..60649a8d5 100644 --- a/packages/web/lib/networking/queries/useGetViewerQuery.tsx +++ b/packages/web/lib/networking/queries/useGetViewerQuery.tsx @@ -1,5 +1,6 @@ import { gql } from 'graphql-request' import useSWR from 'swr' +import { Feature, featureFragment } from '../fragments/featureFragment' import { publicGqlFetcher } from '../networkHelpers' type ViewerQueryResponse = { @@ -22,6 +23,7 @@ export type UserBasicData = { source: string intercomHash: string features: string[] + featureList: Feature[] } export type UserProfile = { @@ -48,8 +50,12 @@ export function useGetViewerQuery(): ViewerQueryResponse { source intercomHash features + featureList { + ...FeatureFields + } } } + ${featureFragment} ` const { data, error, mutate } = useSWR(query, publicGqlFetcher) diff --git a/packages/web/pages/settings/features/beta.tsx b/packages/web/pages/settings/features/beta.tsx index 21ab7f17a..b24e17df9 100644 --- a/packages/web/pages/settings/features/beta.tsx +++ b/packages/web/pages/settings/features/beta.tsx @@ -6,6 +6,7 @@ import { HStack, VStack } from '../../../components/elements/LayoutPrimitives' import { StyledText } from '../../../components/elements/StyledText' import { SettingsLayout } from '../../../components/templates/SettingsLayout' import { styled } from '../../../components/tokens/stitches.config' +import { userHasFeature } from '../../../lib/featureFlag' import { optInFeature } from '../../../lib/networking/mutations/optIntoFeatureMutation' import { useGetViewerQuery } from '../../../lib/networking/queries/useGetViewerQuery' import { applyStoredTheme } from '../../../lib/themeUpdater' @@ -43,13 +44,11 @@ export default function Account(): JSX.Element { ) const hasYouTube = useMemo(() => { - return ( - (viewerData?.me?.features.indexOf('youtube-transcripts') ?? -1) !== -1 - ) + return userHasFeature(viewerData?.me, 'youtube-transcripts') }, [viewerData]) const hasNotion = useMemo(() => { - return (viewerData?.me?.features.indexOf('notion') ?? -1) !== -1 + return userHasFeature(viewerData?.me, 'notion') }, [viewerData]) applyStoredTheme()