diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index a63988225..30cddb636 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -2042,6 +2042,7 @@ export type OptInFeatureError = { export enum OptInFeatureErrorCode { BadRequest = 'BAD_REQUEST', + Ineligible = 'INELIGIBLE', NotFound = 'NOT_FOUND' } diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 1c0800f5b..e432d7f10 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -1527,6 +1527,7 @@ type OptInFeatureError { enum OptInFeatureErrorCode { BAD_REQUEST + INELIGIBLE NOT_FOUND } diff --git a/packages/api/src/resolvers/features/index.ts b/packages/api/src/resolvers/features/index.ts index 6f92d922c..6bb12c2cb 100644 --- a/packages/api/src/resolvers/features/index.ts +++ b/packages/api/src/resolvers/features/index.ts @@ -6,6 +6,7 @@ import { } from '../../generated/graphql' import { getFeatureName, + isOptInFeatureErrorCode, optInFeature, signFeatureToken, } from '../../services/features' @@ -34,9 +35,9 @@ export const optInFeatureResolver = authorized< } const optedInFeature = await optInFeature(featureName, claims.uid) - if (!optedInFeature) { + if (isOptInFeatureErrorCode(optedInFeature)) { return { - errorCodes: [OptInFeatureErrorCode.NotFound], + errorCodes: [optedInFeature], } } log.info('Opted in to a feature', optedInFeature) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 30f13320a..49802bda8 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -2139,6 +2139,7 @@ const schema = gql` enum OptInFeatureErrorCode { BAD_REQUEST NOT_FOUND + INELIGIBLE } union RulesResult = RulesSuccess | RulesError diff --git a/packages/api/src/services/features.ts b/packages/api/src/services/features.ts index 665840306..a6788ea2a 100644 --- a/packages/api/src/services/features.ts +++ b/packages/api/src/services/features.ts @@ -5,11 +5,14 @@ import { Feature } from '../entity/feature' import { env } from '../env' import { getRepository } from '../repository' import { logger } from '../utils/logger' +import { OptInFeatureErrorCode } from '../generated/graphql' +import { Subscription, SubscriptionStatus } from '../entity/subscription' +import { libraryItemRepository } from '../repository/library_item' const MAX_ULTRA_REALISTIC_USERS = 1500 const MAX_YOUTUBE_TRANSCRIPT_USERS = 500 const MAX_NOTION_USERS = 1000 -const MAX_AIDIGEST_USERS = 10 +const MAX_AIDIGEST_USERS = 1000 export enum FeatureName { AISummaries = 'ai-summaries', @@ -20,6 +23,14 @@ export enum FeatureName { AIExplain = 'ai-explain', } +export function isOptInFeatureErrorCode( + value: Feature | OptInFeatureErrorCode +): value is OptInFeatureErrorCode { + return Object.values(OptInFeatureErrorCode).includes( + value as OptInFeatureErrorCode + ) +} + export const getFeatureName = (name: string): FeatureName | undefined => { return Object.values(FeatureName).find((v) => v === name) } @@ -27,7 +38,7 @@ export const getFeatureName = (name: string): FeatureName | undefined => { export const optInFeature = async ( name: FeatureName, uid: string -): Promise => { +): Promise => { switch (name) { case FeatureName.UltraRealisticVoice: return optInLimitedFeature( @@ -44,9 +55,13 @@ export const optInFeature = async ( case FeatureName.Notion: return optInLimitedFeature(FeatureName.Notion, uid, MAX_NOTION_USERS) case FeatureName.AIDigest: + const eligible = await userDigestEligible(uid) + if (!eligible) { + return OptInFeatureErrorCode.Ineligible + } return optInLimitedFeature(FeatureName.AIDigest, uid, MAX_AIDIGEST_USERS) default: - return undefined + return OptInFeatureErrorCode.NotFound } } @@ -157,3 +172,13 @@ export const createFeature = async (feature: DeepPartial) => { export const createFeatures = async (features: DeepPartial[]) => { return getRepository(Feature).save(features) } + +export const userDigestEligible = async (uid: string): Promise => { + const subscriptionsCount = await getRepository(Subscription).count({ + where: { user: { id: uid }, status: SubscriptionStatus.Active }, + }) + const libraryItemsCount = await libraryItemRepository.count({ + where: { user: { id: uid } }, + }) + return subscriptionsCount > 2 && libraryItemsCount > 10 +} diff --git a/packages/web/lib/networking/mutations/optIntoFeatureMutation.ts b/packages/web/lib/networking/mutations/optIntoFeatureMutation.ts index 27633c322..71afdaee8 100644 --- a/packages/web/lib/networking/mutations/optIntoFeatureMutation.ts +++ b/packages/web/lib/networking/mutations/optIntoFeatureMutation.ts @@ -2,12 +2,24 @@ import { gql } from 'graphql-request' import { Feature, featureFragment } from '../fragments/featureFragment' import { gqlFetcher } from '../networkHelpers' +export enum OptInFeatureErrorCode { + BadRequest = 'BAD_REQUEST', + Ineligible = 'INELIGIBLE', + NotFound = 'NOT_FOUND', +} + +export type OptInResult = { + feature?: Feature + ineligible?: boolean +} + export interface OptInFeatureInput { name: string } -export interface OptInFeatureResponse { +interface OptInFeatureResponse { feature?: Feature + errorCodes?: OptInFeatureErrorCode[] } interface Response { @@ -16,7 +28,7 @@ interface Response { export async function optInFeature( input: OptInFeatureInput -): Promise { +): Promise { const mutation = gql` mutation OptInFeature($input: OptInFeatureInput!) { optInFeature(input: $input) { @@ -37,15 +49,29 @@ export async function optInFeature( input, }) const output = data as Response | undefined + console.log('output: ', output, output?.optInFeature?.errorCodes) + if ( + output?.optInFeature?.errorCodes && + output.optInFeature?.errorCodes?.indexOf( + OptInFeatureErrorCode.Ineligible + ) !== -1 + ) { + return { + ineligible: true, + } + } if ( !output || !output.optInFeature.feature || !output.optInFeature.feature.grantedAt ) { - return false + return {} + } + return { + feature: output.optInFeature.feature, } - return true } catch (err) { - return undefined + console.log('error opting into feature') + return {} } } diff --git a/packages/web/pages/settings/account.tsx b/packages/web/pages/settings/account.tsx index bf5640818..3cf0f6a84 100644 --- a/packages/web/pages/settings/account.tsx +++ b/packages/web/pages/settings/account.tsx @@ -525,7 +525,8 @@ const BetaFeaturesSection = (): JSX.Element => { } const DigestSection = (): JSX.Element => { - const { viewerData, isLoading, mutate } = useGetViewerQuery() + const [optInError, setOptInError] = useState(undefined) + const { viewerData, mutate } = useGetViewerQuery() const [channelState, setChannelState] = useState({ push: false, email: false, @@ -603,14 +604,24 @@ const DigestSection = (): JSX.Element => { const requestDigestAccess = useCallback(() => { ;(async () => { const result = await optInFeature({ name: 'ai-digest' }) - if (!result) { + if (result.ineligible) { + setOptInError( + 'To enable digest you need to have saved at least ten library items and have two active subscriptions.' + ) + showErrorToast('You are not eligible for Digest') + } else if (!result.feature) { showErrorToast('Error enabling digest') - return + } else { + setOptInError(undefined) } mutate() })() }, []) + const noChannelsSelected = useMemo(() => { + return !channelState.email && !channelState.library && !channelState.push + }, [channelState]) + return ( { {hasDigest && ( <> + {noChannelsSelected && ( + + You are opted into Omnivore Digest, please make sure to pick at + least one channel for your digest delivery. + + )} { )} + {optInError && ( + + {optInError} + + )} {!hasDigest && (