From 3fc4cabf7eaa392ca276e10da373d16e8c7c6754 Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Mon, 13 Nov 2023 11:06:23 +0100 Subject: [PATCH] chore: Users with noTemplateLimit flag can use paid templates (#9160) * chore: Users with noTemplateLimit flag can use paid templates * Don't show a lock if a user can access premium templates regardless whether it's because their tier or a feature flag --- .../ActivityDetails/TemplateDetails.tsx | 3 +- .../ActivityDetailsSidebar.tsx | 5 +++- .../components/ReflectTemplateDetails.tsx | 12 +++++++- .../meeting/components/SelectTemplate.tsx | 5 ++-- .../client/utils/useTemplateDescription.ts | 22 +++++++++++++-- .../graphql/mutations/selectTemplate.ts | 13 ++++++--- .../helpers/resolveSelectedTemplate.ts | 28 +++++++++++++++---- 7 files changed, 72 insertions(+), 16 deletions(-) diff --git a/packages/client/components/ActivityLibrary/ActivityDetails/TemplateDetails.tsx b/packages/client/components/ActivityLibrary/ActivityDetails/TemplateDetails.tsx index bc0ee547869..0c2b77bef15 100644 --- a/packages/client/components/ActivityLibrary/ActivityDetails/TemplateDetails.tsx +++ b/packages/client/components/ActivityLibrary/ActivityDetails/TemplateDetails.tsx @@ -154,6 +154,7 @@ export const TemplateDetails = (props: Props) => { teams { ...TeamPickerModal_teams } + ...useTemplateDescription_viewer } `, viewerRef @@ -217,7 +218,7 @@ export const TemplateDetails = (props: Props) => { const isOwner = viewerLowestScope === 'TEAM' - const description = useTemplateDescription(viewerLowestScope, activity, tier) + const description = useTemplateDescription(viewerLowestScope, activity, tier, viewer) useEffect(() => { setIsEditing(!!history.location.state?.edit) diff --git a/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx b/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx index 3fceffb2256..5d8e873f25e 100644 --- a/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx +++ b/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx @@ -72,6 +72,7 @@ const ActivityDetailsSidebar = (props: Props) => { featureFlags { gcal adHocTeams + noTemplateLimit } ...AdhocTeamMultiSelect_viewer organizations { @@ -342,7 +343,9 @@ const ActivityDetailsSidebar = (props: Props) => { /> )} - {selectedTeam.tier === 'starter' && !selectedTemplate.isFree ? ( + {selectedTeam.tier === 'starter' && + !viewer.featureFlags.noTemplateLimit && + !selectedTemplate.isFree ? (
Upgrade to the Team Plan to create custom activities unlocking your team’s diff --git a/packages/client/modules/meeting/components/ReflectTemplateDetails.tsx b/packages/client/modules/meeting/components/ReflectTemplateDetails.tsx index b836b084977..dec65509954 100644 --- a/packages/client/modules/meeting/components/ReflectTemplateDetails.tsx +++ b/packages/client/modules/meeting/components/ReflectTemplateDetails.tsx @@ -95,6 +95,14 @@ const ReflectTemplateDetails = (props: Props) => { id orgId tier + viewerTeamMember { + user { + id + featureFlags { + noTemplateLimit + } + } + } } } `, @@ -103,7 +111,8 @@ const ReflectTemplateDetails = (props: Props) => { const {teamTemplates, team} = settings const activeTemplate = settings.activeTemplate ?? settings.selectedTemplate const {id: templateId, name: templateName, prompts, illustrationUrl} = activeTemplate - const {id: teamId, orgId, tier} = team + const {id: teamId, orgId, tier, viewerTeamMember} = team + const noTemplateLimit = viewerTeamMember?.user?.featureFlags?.noTemplateLimit const lowestScope = getTemplateList(teamId, orgId, activeTemplate) const isOwner = activeTemplate.teamId === teamId const description = useTemplateDescription(lowestScope, activeTemplate, tier) @@ -158,6 +167,7 @@ const ReflectTemplateDetails = (props: Props) => { template={activeTemplate} teamId={teamId} tier={tier} + noTemplateLimit={noTemplateLimit} orgId={orgId} /> )} diff --git a/packages/client/modules/meeting/components/SelectTemplate.tsx b/packages/client/modules/meeting/components/SelectTemplate.tsx index 593f9ffeb21..d101abe3304 100644 --- a/packages/client/modules/meeting/components/SelectTemplate.tsx +++ b/packages/client/modules/meeting/components/SelectTemplate.tsx @@ -54,11 +54,12 @@ interface Props { template: SelectTemplate_template$key teamId: string tier?: TierEnum + noTemplateLimit?: boolean orgId?: string } const SelectTemplate = (props: Props) => { - const {template: templateRef, closePortal, teamId, tier, orgId} = props + const {template: templateRef, closePortal, teamId, tier, noTemplateLimit, orgId} = props const template = useFragment( graphql` fragment SelectTemplate_template on MeetingTemplate { @@ -90,7 +91,7 @@ const SelectTemplate = (props: Props) => { }) history.push(`/me/organizations/${orgId}`) } - const showUpgradeCTA = !isFree && tier === 'starter' && scope === 'PUBLIC' + const showUpgradeCTA = !isFree && tier === 'starter' && scope === 'PUBLIC' && !noTemplateLimit if (showUpgradeCTA) { return ( diff --git a/packages/client/utils/useTemplateDescription.ts b/packages/client/utils/useTemplateDescription.ts index 8c36565222c..ffb16050082 100644 --- a/packages/client/utils/useTemplateDescription.ts +++ b/packages/client/utils/useTemplateDescription.ts @@ -1,13 +1,15 @@ import graphql from 'babel-plugin-relay/macro' import {readInlineData} from 'react-relay' import {useTemplateDescription_template$key} from '../__generated__/useTemplateDescription_template.graphql' +import {useTemplateDescription_viewer$key} from '../__generated__/useTemplateDescription_viewer.graphql' import {TierEnum} from '../__generated__/NewMeetingQuery.graphql' import relativeDate from './date/relativeDate' const useTemplateDescription = ( lowestScope: string, templateRef?: useTemplateDescription_template$key, - tier?: TierEnum + tier?: TierEnum, + viewerRef?: useTemplateDescription_viewer$key ) => { if (!templateRef) { return null @@ -27,10 +29,26 @@ const useTemplateDescription = ( templateRef ) + const viewer = readInlineData( + graphql` + fragment useTemplateDescription_viewer on User @inline { + id + featureFlags { + noTemplateLimit + } + } + `, + viewerRef ?? null + ) + const {lastUsedAt, team, isFree} = template const {name: teamName} = team if (lowestScope === 'PUBLIC') { - return isFree ? 'Free template' : `Premium template ${tier === 'starter' ? '🔒' : '✨'}` + return isFree + ? 'Free template' + : `Premium template ${ + tier === 'starter' && !viewer?.featureFlags?.noTemplateLimit ? '🔒' : '✨' + }` } if (lowestScope === 'TEAM') return lastUsedAt diff --git a/packages/server/graphql/mutations/selectTemplate.ts b/packages/server/graphql/mutations/selectTemplate.ts index bb4a8dd5ca6..1264df32f14 100644 --- a/packages/server/graphql/mutations/selectTemplate.ts +++ b/packages/server/graphql/mutations/selectTemplate.ts @@ -30,9 +30,10 @@ const selectTemplate = { const viewerId = getUserId(authToken) // AUTH - const template = (await dataLoader - .get('meetingTemplates') - .load(selectedTemplateId)) as MeetingTemplate + const [template, viewer] = await Promise.all([ + dataLoader.get('meetingTemplates').load(selectedTemplateId) as Promise, + dataLoader.get('users').loadNonNull(viewerId) + ]) if (!template || !template.isActive) { console.log('no template', selectedTemplateId, template) @@ -50,7 +51,11 @@ const selectTemplate = { return standardError(new Error('Template is scoped to organization'), {userId: viewerId}) } } else if (scope === 'PUBLIC') { - if (!isFree && viewerTeam.tier === 'starter') { + if ( + !isFree && + !viewer.featureFlags.includes('noTemplateLimit') && + viewerTeam.tier === 'starter' + ) { return standardError(new Error('User does not have access to this premium template'), { userId: viewerId }) diff --git a/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts b/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts index 9c49c9e9a7a..aa57923b70d 100644 --- a/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts +++ b/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts @@ -2,22 +2,40 @@ import getRethink from '../../../database/rethinkDriver' import MeetingSettingsPoker from '../../../database/types/MeetingSettingsPoker' import MeetingSettingsRetrospective from '../../../database/types/MeetingSettingsRetrospective' import {GQLContext} from '../../graphql' +import {getUserId} from '../../../utils/authorization' +import isValid from '../../isValid' const resolveSelectedTemplate = (fallbackTemplateId: string) => async ( source: MeetingSettingsPoker | MeetingSettingsRetrospective, _args: unknown, - {dataLoader}: GQLContext + {authToken, dataLoader}: GQLContext ) => { + const viewerId = getUserId(authToken) const {id: settingsId, selectedTemplateId, teamId} = source - const [team, template] = await Promise.all([ + const [team, template, viewer] = await Promise.all([ dataLoader.get('teams').loadNonNull(teamId), - dataLoader.get('meetingTemplates').load(selectedTemplateId) + dataLoader.get('meetingTemplates').load(selectedTemplateId), + dataLoader.get('users').loadNonNull(viewerId) ]) const {tier} = team - if (template?.isFree || template?.scope !== 'PUBLIC' || tier !== 'starter') { - return template + if (template) { + if ( + template.isFree || + template.scope !== 'PUBLIC' || + tier !== 'starter' || + viewer.featureFlags.includes('noTemplateLimit') + ) { + return template + } + // if anyone on the team has the noTemplateLimit flag, they might have selected a non-starter template + const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId) + const userIds = teamMembers.map(({userId}) => userId) + const users = (await dataLoader.get('users').loadMany(userIds)).filter(isValid) + if (users.some(({featureFlags}) => featureFlags.includes('noTemplateLimit'))) { + return template + } } // there may be holes in our template deletion or reselection logic, so doing this to be safe source.selectedTemplateId = fallbackTemplateId