diff --git a/packages/api/package.json b/packages/api/package.json
index 8d4bad8d2..f9815dca4 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -45,7 +45,6 @@
"@sendgrid/mail": "^7.6.0",
"@sentry/integrations": "^7.10.0",
"@sentry/node": "^5.26.0",
- "@sentry/tracing": "^7.9.0",
"@types/showdown": "^2.0.6",
"addressparser": "^1.0.1",
"apollo-datasource": "^3.3.1",
@@ -169,4 +168,4 @@
"volta": {
"extends": "../../package.json"
}
-}
+}
\ No newline at end of file
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/jobs/ai/create_digest.ts b/packages/api/src/jobs/ai/create_digest.ts
index 76d38fe69..2bcb1a268 100644
--- a/packages/api/src/jobs/ai/create_digest.ts
+++ b/packages/api/src/jobs/ai/create_digest.ts
@@ -573,7 +573,7 @@ const sendPushNotification = async (userId: string, digest: Digest) => {
const sendEmail = async (user: User, digest: Digest, channels: Channel[]) => {
const title = `${AUTHOR} ${new Date().toLocaleDateString()}`
- const subTitle = truncate(digest.title, { length: 200 }).slice(
+ const subTitle = truncate(digest.title, { length: 65 }).slice(
AUTHOR.length + 1
)
const isInLibrary = channels.includes('library')
@@ -581,7 +581,7 @@ const sendEmail = async (user: User, digest: Digest, channels: Channel[]) => {
const chapters = digest.chapters ?? []
const html = `
-
+
${title}
${subTitle}
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..18b05a0ed 100644
--- a/packages/api/src/services/features.ts
+++ b/packages/api/src/services/features.ts
@@ -3,13 +3,17 @@ import { DeepPartial, FindOptionsWhere, IsNull, Not } from 'typeorm'
import { appDataSource } from '../data_source'
import { Feature } from '../entity/feature'
import { env } from '../env'
-import { getRepository } from '../repository'
+import { authTrx, 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'
+import { LibraryItem } from '../entity/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 +24,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 +39,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(
@@ -43,10 +55,15 @@ export const optInFeature = async (
)
case FeatureName.Notion:
return optInLimitedFeature(FeatureName.Notion, uid, MAX_NOTION_USERS)
- case FeatureName.AIDigest:
+ 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 +174,27 @@ 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 authTrx(
+ async (tx) => {
+ return tx.getRepository(Subscription).count({
+ where: { user: { id: uid }, status: SubscriptionStatus.Active },
+ })
+ },
+ undefined,
+ uid
+ )
+
+ const libraryItemsCount = await authTrx(
+ async (tx) => {
+ return tx.getRepository(LibraryItem).count({
+ where: { user: { id: uid } },
+ })
+ },
+ undefined,
+ 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 && (