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 { 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">
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user