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:
@@ -19,6 +19,8 @@ import { useFuelLogs } from '../hooks/useFuelLogs';
|
||||
import { useUserSettings } from '../hooks/useUserSettings';
|
||||
import { useReceiptOcr } from '../hooks/useReceiptOcr';
|
||||
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 { 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
|
||||
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
|
||||
const {
|
||||
isCapturing,
|
||||
@@ -217,9 +224,16 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
|
||||
}}
|
||||
>
|
||||
<ReceiptCameraButton
|
||||
onClick={startCapture}
|
||||
onClick={() => {
|
||||
if (!hasReceiptScanAccess) {
|
||||
setShowUpgradeDialog(true);
|
||||
return;
|
||||
}
|
||||
startCapture();
|
||||
}}
|
||||
disabled={isProcessing || isLoading}
|
||||
variant="button"
|
||||
locked={!hasReceiptScanAccess}
|
||||
/>
|
||||
</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 */}
|
||||
{ocrError && (
|
||||
<Dialog open={!!ocrError} onClose={resetOcr} maxWidth="xs">
|
||||
|
||||
@@ -7,6 +7,7 @@ import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useTheme, useMediaQuery } from '@mui/material';
|
||||
import CameraAltIcon from '@mui/icons-material/CameraAlt';
|
||||
import ReceiptIcon from '@mui/icons-material/Receipt';
|
||||
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||
|
||||
export interface ReceiptCameraButtonProps {
|
||||
/** Called when user clicks to start capture */
|
||||
@@ -17,6 +18,8 @@ export interface ReceiptCameraButtonProps {
|
||||
variant?: 'icon' | 'button' | 'auto';
|
||||
/** Size of the button */
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
/** Whether the feature is locked behind a tier gate */
|
||||
locked?: boolean;
|
||||
}
|
||||
|
||||
export const ReceiptCameraButton: React.FC<ReceiptCameraButtonProps> = ({
|
||||
@@ -24,6 +27,7 @@ export const ReceiptCameraButton: React.FC<ReceiptCameraButtonProps> = ({
|
||||
disabled = false,
|
||||
variant = 'auto',
|
||||
size = 'medium',
|
||||
locked = false,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
@@ -31,28 +35,30 @@ export const ReceiptCameraButton: React.FC<ReceiptCameraButtonProps> = ({
|
||||
// Determine display variant
|
||||
const displayVariant = variant === 'auto' ? (isMobile ? 'icon' : 'button') : variant;
|
||||
|
||||
const tooltipTitle = locked ? 'Upgrade to Pro to scan receipts' : 'Scan Receipt';
|
||||
|
||||
if (displayVariant === 'icon') {
|
||||
return (
|
||||
<Tooltip title="Scan Receipt">
|
||||
<Tooltip title={tooltipTitle}>
|
||||
<span>
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
color="primary"
|
||||
size={size}
|
||||
aria-label="Scan receipt with camera"
|
||||
aria-label={locked ? 'Scan receipt (Pro feature)' : 'Scan receipt with camera'}
|
||||
sx={{
|
||||
backgroundColor: 'primary.light',
|
||||
color: 'primary.contrastText',
|
||||
backgroundColor: locked ? 'action.disabledBackground' : 'primary.light',
|
||||
color: locked ? 'text.secondary' : 'primary.contrastText',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.main',
|
||||
backgroundColor: locked ? 'action.hover' : 'primary.main',
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
backgroundColor: 'action.disabledBackground',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CameraAltIcon />
|
||||
{locked ? <LockOutlinedIcon /> : <CameraAltIcon />}
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -60,23 +66,27 @@ export const ReceiptCameraButton: React.FC<ReceiptCameraButtonProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size={size}
|
||||
startIcon={<ReceiptIcon />}
|
||||
endIcon={<CameraAltIcon />}
|
||||
sx={{
|
||||
borderStyle: 'dashed',
|
||||
'&:hover': {
|
||||
borderStyle: 'solid',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Scan Receipt
|
||||
</Button>
|
||||
<Tooltip title={locked ? tooltipTitle : ''}>
|
||||
<span>
|
||||
<Button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
variant="outlined"
|
||||
color={locked ? 'inherit' : 'primary'}
|
||||
size={size}
|
||||
startIcon={locked ? <LockOutlinedIcon /> : <ReceiptIcon />}
|
||||
endIcon={locked ? undefined : <CameraAltIcon />}
|
||||
sx={{
|
||||
borderStyle: 'dashed',
|
||||
'&:hover': {
|
||||
borderStyle: 'solid',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{locked ? 'Scan Receipt (Pro)' : 'Scan Receipt'}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user