Only allow eligible users to enable digest
This commit is contained in:
@ -2042,6 +2042,7 @@ export type OptInFeatureError = {
|
||||
|
||||
export enum OptInFeatureErrorCode {
|
||||
BadRequest = 'BAD_REQUEST',
|
||||
Ineligible = 'INELIGIBLE',
|
||||
NotFound = 'NOT_FOUND'
|
||||
}
|
||||
|
||||
|
||||
@ -1527,6 +1527,7 @@ type OptInFeatureError {
|
||||
|
||||
enum OptInFeatureErrorCode {
|
||||
BAD_REQUEST
|
||||
INELIGIBLE
|
||||
NOT_FOUND
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -2139,6 +2139,7 @@ const schema = gql`
|
||||
enum OptInFeatureErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
INELIGIBLE
|
||||
}
|
||||
|
||||
union RulesResult = RulesSuccess | RulesError
|
||||
|
||||
@ -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<Feature | undefined> => {
|
||||
): Promise<Feature | OptInFeatureErrorCode> => {
|
||||
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<Feature>) => {
|
||||
export const createFeatures = async (features: DeepPartial<Feature>[]) => {
|
||||
return getRepository(Feature).save(features)
|
||||
}
|
||||
|
||||
export const userDigestEligible = async (uid: string): Promise<boolean> => {
|
||||
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
|
||||
}
|
||||
|
||||
@ -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<boolean | undefined> {
|
||||
): Promise<OptInResult> {
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -525,7 +525,8 @@ const BetaFeaturesSection = (): JSX.Element => {
|
||||
}
|
||||
|
||||
const DigestSection = (): JSX.Element => {
|
||||
const { viewerData, isLoading, mutate } = useGetViewerQuery()
|
||||
const [optInError, setOptInError] = useState<string | undefined>(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 (
|
||||
<VStack
|
||||
css={{
|
||||
@ -652,6 +663,20 @@ const DigestSection = (): JSX.Element => {
|
||||
</StyledText>
|
||||
{hasDigest && (
|
||||
<>
|
||||
{noChannelsSelected && (
|
||||
<StyledText
|
||||
style="error"
|
||||
css={{
|
||||
display: 'flex',
|
||||
gap: '5px',
|
||||
lineHeight: '22px',
|
||||
mt: '0px',
|
||||
}}
|
||||
>
|
||||
You are opted into Omnivore Digest, please make sure to pick at
|
||||
least one channel for your digest delivery.
|
||||
</StyledText>
|
||||
)}
|
||||
<StyledText
|
||||
style="footnote"
|
||||
css={{ display: 'flex', gap: '5px', m: '0px' }}
|
||||
@ -696,6 +721,14 @@ const DigestSection = (): JSX.Element => {
|
||||
</StyledText>
|
||||
</>
|
||||
)}
|
||||
{optInError && (
|
||||
<StyledText
|
||||
style="error"
|
||||
css={{ display: 'flex', gap: '5px', m: '0px', mb: '5px' }}
|
||||
>
|
||||
{optInError}
|
||||
</StyledText>
|
||||
)}
|
||||
{!hasDigest && (
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
|
||||
@ -5,9 +5,9 @@ import { Box } from '../../components/elements/LayoutPrimitives'
|
||||
export default function Extensions(): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (window.innerWidth <= 1024) {
|
||||
Router.push('/settings/installation/mobile')
|
||||
Router.push('/settings/account')
|
||||
} else {
|
||||
Router.push('/settings/installation/extensions')
|
||||
Router.push('/settings/account')
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user