Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/api/ebbApi/flowSessionApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Workflow, WorkflowApi } from './workflowApi'
import { slackApi } from './slackApi'
import { logAndToastError } from '@/lib/utils/ebbError.util'
import { WorkflowDb } from '../../db/ebb/workflowRepo'
import { usePermissionsStore } from '@/lib/stores/permissionsStore'

export type FlowSessionWithWorkflow = FlowSessionWithWorkflowDb & {
workflow_json?: Workflow
Expand All @@ -13,10 +14,12 @@ const startFlowSession = async (
objective: string,
type: 'smart' | 'manual',
workflow?: Workflow | null,

): Promise<string> => {
const inProgressFlowSession = await FlowSessionRepo.getInProgressFlowSession()

if (inProgressFlowSession) throw new Error('Flow session already in progress')
const { permissions } = usePermissionsStore.getState()

let workflowToUse = workflow
if (!workflow) {
Expand All @@ -37,7 +40,7 @@ const startFlowSession = async (
await FlowSessionRepo.createFlowSession(flowSession)

// Handle Slack DND if enabled
if (workflowToUse.settings.slack?.dndEnabled) {
if (permissions.canUseSlackIntegration && workflowToUse.settings.slack?.dndEnabled) {
const durationMinutes = workflowToUse.settings.defaultDuration || 25
slackApi.startFocusSession(durationMinutes).catch(error => {
logAndToastError('Failed to enable Slack DND for all workspaces', error)
Expand Down
9 changes: 9 additions & 0 deletions src/api/ebbApi/licenseApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export interface LicensePermissions {
canUseAllowList: boolean
canUseMultipleProfiles: boolean
canUseMultipleDevices: boolean
canUseSmartFocus: boolean
canUseSlackIntegration: boolean
canScheduleSessions: boolean
hasProAccess: boolean
}

Expand All @@ -52,6 +55,9 @@ export const defaultPermissions: LicensePermissions = {
canUseAllowList: false,
canUseMultipleProfiles: false,
canUseMultipleDevices: false,
canUseSmartFocus: false,
canUseSlackIntegration: false,
canScheduleSessions: false,
hasProAccess: false,
}

Expand Down Expand Up @@ -84,6 +90,9 @@ const calculatePermissions = (license: License | null): LicensePermissions | nul
canUseAllowList: hasProAccess,
canUseMultipleProfiles: hasProAccess,
canUseMultipleDevices: hasProAccess,
canUseSmartFocus: hasProAccess,
canUseSlackIntegration: hasProAccess,
canScheduleSessions: hasProAccess,
hasProAccess,
}
}
Expand Down
82 changes: 42 additions & 40 deletions src/components/AppSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DifficultySelector } from '@/components/difficulty-selector'
import { CategoryTooltip } from '@/components/CategoryTooltip'
import { usePermissions } from '@/hooks/usePermissions'
import { usePaywall } from '@/hooks/usePaywall'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'

interface CategoryOption {
type: 'category'
Expand Down Expand Up @@ -86,7 +87,7 @@ const getOptionDetails = (option: SearchOption) => {
}

export function AppSelector({
placeholder = 'Search apps & websites...',
placeholder,
emptyText = 'Enter full URL to add website',
maxItems = 5,
selectedApps,
Expand Down Expand Up @@ -117,6 +118,12 @@ export function AppSelector({
const [selected, setSelected] = useState(0)
const [apps, setApps] = useState<AppOption[]>([])
const [categoryOptions, setCategoryOptions] = useState<CategoryOption[]>([])

// Dynamic placeholder based on allowlist/blocklist mode
const dynamicPlaceholder = placeholder || (isAllowList
? 'Search apps & websites to allow...'
: 'Search apps & websites to block...')

const handleClose = () => {
setOpen(false)
setSearch('')
Expand Down Expand Up @@ -487,7 +494,7 @@ export function AppSelector({

<div className="relative flex-1 min-w-[200px]">
<input
placeholder={placeholder}
placeholder={dynamicPlaceholder}
value={search}
onChange={(e) => setSearch(e.target.value)}
onFocus={() => setOpen(true)}
Expand Down Expand Up @@ -535,44 +542,39 @@ export function AppSelector({
Block
</AnalyticsButton>

{!canUseAllowList ? (
<AnalyticsButton
variant="ghost"
size="sm"
className={cn(
'h-6 px-2 text-xs text-muted-foreground/80 hover:text-foreground',
isAllowList && 'bg-muted/50'
)}
analyticsEvent="allow_list_clicked"
analyticsProperties={{
context: 'allow_list',
button_location: 'app_selector'
}}
onClick={openPaywall}
>
Allow
</AnalyticsButton>
) : (
<AnalyticsButton
variant="ghost"
size="sm"
className={cn(
'h-6 px-2 text-xs text-muted-foreground/80 hover:text-foreground',
isAllowList && 'bg-muted/50'
)}
onClick={(e) => {
e.stopPropagation()
handleAllowListChange(true)
}}
analyticsEvent="allow_list_clicked"
analyticsProperties={{
context: 'allow_list',
button_location: 'app_selector'
}}
>
Allow
</AnalyticsButton>
)}
<Tooltip>
<TooltipTrigger asChild>
<AnalyticsButton
variant="ghost"
size="sm"
className={cn(
'h-6 px-2 text-xs text-muted-foreground/80 hover:text-foreground',
!canUseAllowList && 'opacity-50',
isAllowList && 'bg-muted/50'
)}
analyticsEvent="allow_list_clicked"
analyticsProperties={{
context: canUseAllowList ? 'allow_list' : 'allow_list_locked',
button_location: 'app_selector'
}}
onClick={(e) => {
e.stopPropagation()
if (!canUseAllowList) {
openPaywall()
} else {
handleAllowListChange(true)
}
}}
>
Allow
</AnalyticsButton>
</TooltipTrigger>
{!canUseAllowList && (
<TooltipContent>
Allow List (Pro)
</TooltipContent>
)}
</Tooltip>
</div>
)}
</div>
Expand Down
28 changes: 28 additions & 0 deletions src/components/ProFeatureOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { RainbowButton } from '@/components/ui/rainbow-button'
import { usePaywall } from '@/hooks/usePaywall'

interface ProFeatureOverlayProps {
title: string
subtitle: string
ctaText?: string
}

export function ProFeatureOverlay({
title,
subtitle,
ctaText = 'Upgrade to Pro'
}: ProFeatureOverlayProps) {
const { openPaywall } = usePaywall()

return (
<div className="absolute inset-0 z-50 flex items-center justify-center backdrop-blur-sm bg-background/80">
<div className="text-center max-w-md px-6">
<h2 className="text-3xl font-bold mb-4">{title}</h2>
<p className="text-muted-foreground text-lg mb-8">{subtitle}</p>
<RainbowButton onClick={openPaywall} className="w-full max-w-xs">
{ctaText}
</RainbowButton>
</div>
</div>
)
}
23 changes: 19 additions & 4 deletions src/components/SlackFocusToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { SlackSettings } from '@/api/ebbApi/workflowApi'
import { useAuth } from '@/hooks/useAuth'
import { useNavigate } from 'react-router-dom'
import { toast } from 'sonner'
import { usePermissions } from '@/hooks/usePermissions'
import { usePaywall } from '@/hooks/usePaywall'

interface SlackFocusToggleProps {
slackSettings: SlackSettings
Expand All @@ -28,6 +30,8 @@ interface SlackFocusToggleProps {
export function SlackFocusToggle({ slackSettings, onSlackSettingsChange }: SlackFocusToggleProps) {
const { user } = useAuth()
const navigate = useNavigate()
const { canUseSlackIntegration } = usePermissions()
const { openPaywall } = usePaywall()

const [showDialog, setShowDialog] = useState(false)
const [customStatusText, setCustomStatusText] = useState('')
Expand All @@ -44,6 +48,12 @@ export function SlackFocusToggle({ slackSettings, onSlackSettingsChange }: Slack
}, [showDialog, slackStatus?.preferences])

const handleSlackToggle = async () => {
// Check permissions first
if (!canUseSlackIntegration) {
openPaywall()
return
}

if (!user) {
toast.error('Must be logged in to use Slack', {
action: {
Expand Down Expand Up @@ -104,6 +114,9 @@ export function SlackFocusToggle({ slackSettings, onSlackSettingsChange }: Slack
}

const getTooltipText = () => {
if (!canUseSlackIntegration) {
return 'Slack Integration (Pro)'
}
if (!user) {
return 'Please login to use the slack integration'
}
Expand All @@ -124,13 +137,15 @@ export function SlackFocusToggle({ slackSettings, onSlackSettingsChange }: Slack
) : (
<Tooltip>
<TooltipTrigger asChild>
<div
<div
onClick={handleSlackToggle}
className="h-9 w-9 rounded-md flex items-center justify-center hover:bg-accent cursor-pointer relative"
className={`h-9 w-9 rounded-md flex items-center justify-center hover:bg-accent cursor-pointer relative ${
!canUseSlackIntegration ? 'opacity-40' : ''
}`}
>
<SlackIcon fill={isActive ? '' : 'currentColor'} className={`h-5 w-5 transition-colors ${
isActive
? 'text-green-500'
isActive
? 'text-green-500'
: 'text-muted-foreground'
}`} />
{isActive && (
Expand Down
13 changes: 11 additions & 2 deletions src/hooks/usePermissions.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { useLicenseWithDevices } from '@/api/hooks/useLicense'
import { useAuth } from './useAuth'
import { defaultPermissions } from '@/api/ebbApi/licenseApi'
import { usePermissionsStore } from '@/lib/stores/permissionsStore'
import { useEffect } from 'react'

export const usePermissions = () => {
const { user } = useAuth()
const { data: licenseData } = useLicenseWithDevices(user?.id || null)
const setPermissions = usePermissionsStore((state) => state.setPermissions)

return licenseData?.permissions || defaultPermissions
}
const permissions = licenseData?.permissions || defaultPermissions

// Sync to store whenever permissions change
useEffect(() => {
setPermissions(permissions)
}, [permissions, setPermissions])

return permissions
}
55 changes: 32 additions & 23 deletions src/hooks/useWorkerPolling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SmartSessionApi } from '@/api/ebbApi/smartSessionApi'
import { ScheduledSessionExecutionApi } from '@/api/ebbApi/scheduledSessionExecutionApi'
import { useAuth } from './useAuth'
import { debug } from '@tauri-apps/plugin-log'
import { usePermissions } from './usePermissions'

type OnlinePingEvent = {
event: string
Expand All @@ -24,6 +25,7 @@ export const useWorkerPolling = () => {
const { deviceId } = useDeviceProfile()
const { mutate: updateProfile } = useUpdateProfile()
const { updateRollupForUser } = useUpdateRollupForUser()
const { canUseSmartFocus, canScheduleSessions } = usePermissions()

useEffect(() => {
if(isLoading || !profile) return
Expand All @@ -45,7 +47,8 @@ export const useWorkerPolling = () => {
updateProfile({ id: profile.id, last_check_in: last_check_in.toISO(), version })
refetch()
}
if(deviceId) {
const handleShouldStartSmartFocus = async () => {
if(!deviceId || !canUseSmartFocus) return
const shouldSuggestSmartSession = await SmartSessionApi.checkShouldSuggestSmartSession(deviceId)
if(shouldSuggestSmartSession === 'smart') {
invoke('show_notification', {
Expand All @@ -59,30 +62,36 @@ export const useWorkerPolling = () => {
}
}

// Check for scheduled sessions
const scheduledSessionStatus = await ScheduledSessionExecutionApi.checkScheduledSessionStatus()
if(scheduledSessionStatus.type === 'reminder') {
const payload = {
workflowId: scheduledSessionStatus.schedule.workflowId,
workflowName: scheduledSessionStatus.schedule.workflowName,
}
invoke('show_notification', {
notificationType: 'scheduled-session-reminder',
payload: JSON.stringify(payload),
})
}
else if(scheduledSessionStatus.type === 'start') {
invoke('show_notification', {
notificationType: 'scheduled-session-start',
payload: JSON.stringify({
const handleShouldStartScheduledSession = async () => {

// Check for scheduled sessions (Pro feature)
if(!canScheduleSessions) return
const scheduledSessionStatus = await ScheduledSessionExecutionApi.checkScheduledSessionStatus()
if(scheduledSessionStatus.type === 'reminder') {
const payload = {
workflowId: scheduledSessionStatus.schedule.workflowId,
workflowName: scheduledSessionStatus.schedule.workflowName,
}),
})
}
}
invoke('show_notification', {
notificationType: 'scheduled-session-reminder',
payload: JSON.stringify(payload),
})
}
else if(scheduledSessionStatus.type === 'start') {
invoke('show_notification', {
notificationType: 'scheduled-session-start',
payload: JSON.stringify({
workflowId: scheduledSessionStatus.schedule.workflowId,
workflowName: scheduledSessionStatus.schedule.workflowName,
}),
})
}

// Clean up old scheduled session tracking periodically
ScheduledSessionExecutionApi.cleanupOldSessionTracking()
// Clean up old scheduled session tracking periodically
ScheduledSessionExecutionApi.cleanupOldSessionTracking()
}
handleShouldStartSmartFocus()
handleShouldStartScheduledSession()
}
EbbWorker.work(event.payload, run) // used to make sure we don't run the same work multiple times

Expand All @@ -97,6 +106,6 @@ export const useWorkerPolling = () => {
debug(`[useWorkerPolling] Failed to unlisten online-ping event: ${error}`)
}
}
}, [profile, isLoading, updateRollupForUser, deviceId, user])
}, [profile, isLoading, updateRollupForUser, deviceId, user, canUseSmartFocus, canScheduleSessions])

}
12 changes: 12 additions & 0 deletions src/lib/stores/permissionsStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { create } from 'zustand'
import {defaultPermissions, LicensePermissions } from '@/api/ebbApi/licenseApi'

interface PermissionsStore {
permissions: LicensePermissions
setPermissions: (permissions: LicensePermissions) => void
}

export const usePermissionsStore = create<PermissionsStore>((set) => ({
permissions: defaultPermissions,
setPermissions: (permissions) => set({ permissions }),
}))
Loading