feat: add tier gating for receipt scan in FuelLogForm (refs #131)

Free tier users see locked button with upgrade prompt dialog.
Pro+ users can capture receipts normally. Works on mobile and desktop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-11 09:32:08 -06:00
parent 399313eb6d
commit bc91fbad79
2 changed files with 55 additions and 24 deletions

View File

@@ -19,6 +19,8 @@ import { useFuelLogs } from '../hooks/useFuelLogs';
import { useUserSettings } from '../hooks/useUserSettings'; import { useUserSettings } from '../hooks/useUserSettings';
import { useReceiptOcr } from '../hooks/useReceiptOcr'; import { useReceiptOcr } from '../hooks/useReceiptOcr';
import { useGeolocation } from '../../stations/hooks/useGeolocation'; import { useGeolocation } from '../../stations/hooks/useGeolocation';
import { useTierAccess } from '../../../core/hooks/useTierAccess';
import { UpgradeRequiredDialog } from '../../../shared-minimal/components/UpgradeRequiredDialog';
import { CameraCapture } from '../../../shared/components/CameraCapture'; import { CameraCapture } from '../../../shared/components/CameraCapture';
import { CreateFuelLogRequest, FuelType } from '../types/fuel-logs.types'; import { CreateFuelLogRequest, FuelType } from '../types/fuel-logs.types';
@@ -48,6 +50,11 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
// Get user location for nearby station search // Get user location for nearby station search
const { coordinates: userLocation } = useGeolocation(); const { coordinates: userLocation } = useGeolocation();
// Tier access check for receipt scan feature
const { hasAccess } = useTierAccess();
const hasReceiptScanAccess = hasAccess('fuelLog.receiptScan');
const [showUpgradeDialog, setShowUpgradeDialog] = useState(false);
// Receipt OCR integration // Receipt OCR integration
const { const {
isCapturing, isCapturing,
@@ -217,9 +224,16 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
}} }}
> >
<ReceiptCameraButton <ReceiptCameraButton
onClick={startCapture} onClick={() => {
if (!hasReceiptScanAccess) {
setShowUpgradeDialog(true);
return;
}
startCapture();
}}
disabled={isProcessing || isLoading} disabled={isProcessing || isLoading}
variant="button" variant="button"
locked={!hasReceiptScanAccess}
/> />
</Box> </Box>
@@ -436,6 +450,13 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
/> />
)} )}
{/* Upgrade Required Dialog for Receipt Scan */}
<UpgradeRequiredDialog
featureKey="fuelLog.receiptScan"
open={showUpgradeDialog}
onClose={() => setShowUpgradeDialog(false)}
/>
{/* OCR Error Display */} {/* OCR Error Display */}
{ocrError && ( {ocrError && (
<Dialog open={!!ocrError} onClose={resetOcr} maxWidth="xs"> <Dialog open={!!ocrError} onClose={resetOcr} maxWidth="xs">

View File

@@ -7,6 +7,7 @@ import React from 'react';
import { Button, IconButton, Tooltip, useTheme, useMediaQuery } from '@mui/material'; import { Button, IconButton, Tooltip, useTheme, useMediaQuery } from '@mui/material';
import CameraAltIcon from '@mui/icons-material/CameraAlt'; import CameraAltIcon from '@mui/icons-material/CameraAlt';
import ReceiptIcon from '@mui/icons-material/Receipt'; import ReceiptIcon from '@mui/icons-material/Receipt';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
export interface ReceiptCameraButtonProps { export interface ReceiptCameraButtonProps {
/** Called when user clicks to start capture */ /** Called when user clicks to start capture */
@@ -17,6 +18,8 @@ export interface ReceiptCameraButtonProps {
variant?: 'icon' | 'button' | 'auto'; variant?: 'icon' | 'button' | 'auto';
/** Size of the button */ /** Size of the button */
size?: 'small' | 'medium' | 'large'; size?: 'small' | 'medium' | 'large';
/** Whether the feature is locked behind a tier gate */
locked?: boolean;
} }
export const ReceiptCameraButton: React.FC<ReceiptCameraButtonProps> = ({ export const ReceiptCameraButton: React.FC<ReceiptCameraButtonProps> = ({
@@ -24,6 +27,7 @@ export const ReceiptCameraButton: React.FC<ReceiptCameraButtonProps> = ({
disabled = false, disabled = false,
variant = 'auto', variant = 'auto',
size = 'medium', size = 'medium',
locked = false,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
@@ -31,28 +35,30 @@ export const ReceiptCameraButton: React.FC<ReceiptCameraButtonProps> = ({
// Determine display variant // Determine display variant
const displayVariant = variant === 'auto' ? (isMobile ? 'icon' : 'button') : variant; const displayVariant = variant === 'auto' ? (isMobile ? 'icon' : 'button') : variant;
const tooltipTitle = locked ? 'Upgrade to Pro to scan receipts' : 'Scan Receipt';
if (displayVariant === 'icon') { if (displayVariant === 'icon') {
return ( return (
<Tooltip title="Scan Receipt"> <Tooltip title={tooltipTitle}>
<span> <span>
<IconButton <IconButton
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
color="primary" color="primary"
size={size} size={size}
aria-label="Scan receipt with camera" aria-label={locked ? 'Scan receipt (Pro feature)' : 'Scan receipt with camera'}
sx={{ sx={{
backgroundColor: 'primary.light', backgroundColor: locked ? 'action.disabledBackground' : 'primary.light',
color: 'primary.contrastText', color: locked ? 'text.secondary' : 'primary.contrastText',
'&:hover': { '&:hover': {
backgroundColor: 'primary.main', backgroundColor: locked ? 'action.hover' : 'primary.main',
}, },
'&.Mui-disabled': { '&.Mui-disabled': {
backgroundColor: 'action.disabledBackground', backgroundColor: 'action.disabledBackground',
}, },
}} }}
> >
<CameraAltIcon /> {locked ? <LockOutlinedIcon /> : <CameraAltIcon />}
</IconButton> </IconButton>
</span> </span>
</Tooltip> </Tooltip>
@@ -60,14 +66,16 @@ export const ReceiptCameraButton: React.FC<ReceiptCameraButtonProps> = ({
} }
return ( return (
<Tooltip title={locked ? tooltipTitle : ''}>
<span>
<Button <Button
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
variant="outlined" variant="outlined"
color="primary" color={locked ? 'inherit' : 'primary'}
size={size} size={size}
startIcon={<ReceiptIcon />} startIcon={locked ? <LockOutlinedIcon /> : <ReceiptIcon />}
endIcon={<CameraAltIcon />} endIcon={locked ? undefined : <CameraAltIcon />}
sx={{ sx={{
borderStyle: 'dashed', borderStyle: 'dashed',
'&:hover': { '&:hover': {
@@ -75,8 +83,10 @@ export const ReceiptCameraButton: React.FC<ReceiptCameraButtonProps> = ({
}, },
}} }}
> >
Scan Receipt {locked ? 'Scan Receipt (Pro)' : 'Scan Receipt'}
</Button> </Button>
</span>
</Tooltip>
); );
}; };