diff --git a/babel.config.js b/babel.config.js index 9d89e13..36da980 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,6 +1,19 @@ module.exports = function (api) { api.cache(true); + + const plugins = []; + + // Remove console.log and console.warn in production builds + // Keep console.error for actual error reporting + if (process.env.NODE_ENV === 'production') { + plugins.push([ + 'transform-remove-console', + {exclude: ['error']}, + ]); + } + return { presets: ['babel-preset-expo'], + plugins, }; }; diff --git a/package-lock.json b/package-lock.json index e491017..01e75a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@types/react": "^19.2.0", "@types/react-native-vector-icons": "^6.4.18", "@types/react-test-renderer": "^19.1.0", + "babel-plugin-transform-remove-console": "^6.9.4", "eslint": "^8.19.0", "jest": "^29.6.3", "prettier": "2.8.8", @@ -5311,6 +5312,13 @@ "@babel/plugin-syntax-flow": "^7.12.1" } }, + "node_modules/babel-plugin-transform-remove-console": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz", + "integrity": "sha512-88blrUrMX3SPiGkT1GnvVY8E/7A+k6oj3MNvUtTIxJflFzXTw1bHkuJ/y039ouhFMp2prRn5cQGzokViYi1dsg==", + "dev": true, + "license": "MIT" + }, "node_modules/babel-preset-current-node-syntax": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", diff --git a/package.json b/package.json index d93b6e7..cf7dd42 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@types/react": "^19.2.0", "@types/react-native-vector-icons": "^6.4.18", "@types/react-test-renderer": "^19.1.0", + "babel-plugin-transform-remove-console": "^6.9.4", "eslint": "^8.19.0", "jest": "^29.6.3", "prettier": "2.8.8", diff --git a/src/hooks/useScan.ts b/src/hooks/useScan.ts index 6ffbba3..cbe62be 100644 --- a/src/hooks/useScan.ts +++ b/src/hooks/useScan.ts @@ -9,6 +9,16 @@ import {detectChip} from '../services/detection'; import type {ScanState, RawTagData, ScanError, NFCStatus} from '../types/nfc'; import type {Transponder} from '../types/detection'; +/** Progress update during scanning */ +export interface ScanProgress { + /** Current step description */ + step: string; + /** Step number (1-indexed) */ + current: number; + /** Total steps (if known) */ + total?: number; +} + export interface UseScanResult { /** Current scan state */ state: ScanState; @@ -20,6 +30,8 @@ export interface UseScanResult { error: ScanError | null; /** NFC status (supported and enabled) */ nfcStatus: NFCStatus; + /** Current scan progress (during 'scanning' state) */ + scanProgress: ScanProgress | null; /** Start scanning for a tag */ startScan: () => Promise; /** Cancel ongoing scan */ @@ -42,6 +54,7 @@ export function useScan(): UseScanResult { isSupported: false, isEnabled: false, }); + const [scanProgress, setScanProgress] = useState(null); // Track if component is mounted to prevent state updates after unmount const isMounted = useRef(true); @@ -83,6 +96,7 @@ export function useScan(): UseScanResult { setTag(null); setTransponder(null); setError(null); + setScanProgress({step: 'Waiting for tag...', current: 1}); try { // Check NFC status first @@ -115,8 +129,23 @@ export function useScan(): UseScanResult { // Perform scan with detection in one session const result = await nfcManager.scanWithDetection(async tagData => { + // Update progress: tag detected + if (isMounted.current) { + setScanProgress({step: 'Tag detected, identifying chip...', current: 2}); + } + // Run chip detection while NFC session is still active - const detectionResult = await detectChip(tagData); + const detectionResult = await detectChip(tagData, step => { + // Progress callback from detection + if (isMounted.current) { + setScanProgress({step, current: 3}); + } + }); + + if (isMounted.current) { + setScanProgress({step: 'Detection complete', current: 4}); + } + return detectionResult.success ? detectionResult.transponder : null; }); @@ -175,6 +204,7 @@ export function useScan(): UseScanResult { setTag(null); setTransponder(null); setError(null); + setScanProgress(null); }, []); // Open device NFC settings @@ -193,6 +223,7 @@ export function useScan(): UseScanResult { transponder, error, nfcStatus, + scanProgress, startScan, cancelScan, reset, diff --git a/src/screens/ResultScreen.tsx b/src/screens/ResultScreen.tsx index e81f900..c789bad 100644 --- a/src/screens/ResultScreen.tsx +++ b/src/screens/ResultScreen.tsx @@ -1,13 +1,16 @@ -import React, {useMemo} from 'react'; -import {StyleSheet, View, ScrollView, Linking} from 'react-native'; -import {Button, Text, Surface, Divider, Chip} from 'react-native-paper'; -import type {ResultScreenProps} from '../types/navigation'; -import {DTColors} from '../theme'; -import {matchChipToProducts, getMatchSummary, getDesfireEvMismatchWarning} from '../services/matching'; -import {Product} from '../types/products'; +import React, { useMemo, useState } from 'react'; +import { StyleSheet, View, ScrollView, Linking, TouchableOpacity } from 'react-native'; +import { Button, Text, Surface, Divider, Chip } from 'react-native-paper'; +import type { ResultScreenProps } from '../types/navigation'; +import { DTColors } from '../theme'; +import { matchChipToProducts, getMatchSummary, getDesfireEvMismatchWarning } from '../services/matching'; +import { Product } from '../types/products'; +import { getChipInfo, getChipFamilyInfo, getSecurityLevelDescription } from '../data/chipInfo'; +import { getChipFamily } from '../types/detection'; -export function ResultScreen({route, navigation}: ResultScreenProps) { - const {tagData, transponder} = route.params; +export function ResultScreen({ route, navigation }: ResultScreenProps) { + const { tagData, transponder } = route.params; + const [showChipInfo, setShowChipInfo] = useState(false); // Match chip to products const matchResult = useMemo(() => { @@ -15,6 +18,17 @@ export function ResultScreen({route, navigation}: ResultScreenProps) { return matchChipToProducts(transponder.type); }, [transponder]); + // Get educational chip info + const chipInfo = useMemo(() => { + if (!transponder) return null; + return getChipInfo(transponder.type); + }, [transponder]); + + const chipFamilyInfo = useMemo(() => { + if (!transponder) return null; + return getChipFamilyInfo(getChipFamily(transponder.type)); + }, [transponder]); + const handleConversionLink = () => { Linking.openURL(matchResult?.conversionUrl || 'https://dngr.us/conversion'); }; @@ -36,7 +50,7 @@ export function ResultScreen({route, navigation}: ResultScreenProps) { medium: DTColors.modeEmphasis, low: DTColors.modeWarning, }; - return {color: colors[transponder.confidence], label: `${transponder.confidence.toUpperCase()} CONFIDENCE`}; + return { color: colors[transponder.confidence], label: `${transponder.confidence.toUpperCase()} CONFIDENCE` }; }; return ( @@ -54,16 +68,39 @@ export function ResultScreen({route, navigation}: ResultScreenProps) { {transponder.chipName} + {/* Show implant name if detected from memory, or payment device */} + {transponder.implantName && ( + transponder.implantName.includes('Payment Card') ? ( + + + PAYMENT DEVICE DETECTED + + + {transponder.implantName.replace(' Payment Card', '')} + + + ) : ( + + + IMPLANT DETECTED + + + {transponder.implantName} + + + ) + )} + {transponder.family} {getConfidenceLabel() && ( + style={[styles.confidenceChip, { borderColor: getConfidenceLabel()!.color }]} + textStyle={[styles.confidenceChipText, { color: getConfidenceLabel()!.color }]}> {getConfidenceLabel()!.label} )} @@ -86,7 +123,7 @@ export function ResultScreen({route, navigation}: ResultScreenProps) { + style={[styles.detailValue, { color: getCloneabilityColor() }]}> {transponder.isCloneable ? 'YES' : 'NO'} @@ -99,13 +136,114 @@ export function ResultScreen({route, navigation}: ResultScreenProps) { )} - {/* Product Matches Card */} - {transponder && matchResult && (matchResult.exactMatches.length > 0 || matchResult.cloneTargets.length > 0) && ( + {/* Educational Chip Info Card */} + {transponder && (chipInfo || chipFamilyInfo) && ( + + setShowChipInfo(!showChipInfo)} + style={styles.chipInfoHeader} + activeOpacity={0.7}> + + ABOUT THIS CHIP + + + {showChipInfo ? '▼' : '▶'} + + + + {showChipInfo && ( + <> + + + {/* Family description */} + {chipFamilyInfo && ( + + {chipFamilyInfo} + + )} + + {/* Detailed chip info */} + {chipInfo && ( + <> + + {chipInfo.description} + + + {chipInfo.memoryNote && ( + + + Memory: + + + {chipInfo.memoryNote} + + + )} + + + + Security: + + + {chipInfo.securityLevel.toUpperCase()} + + + + + {getSecurityLevelDescription(chipInfo.securityLevel)} + + + {chipInfo.commonUses.length > 0 && ( + <> + + COMMON USES + + {chipInfo.commonUses.map((use, idx) => ( + + • {use} + + ))} + + )} + + {chipInfo.capabilities.length > 0 && ( + <> + + CAPABILITIES + + {chipInfo.capabilities.map((cap, idx) => ( + + • {cap} + + ))} + + )} + + )} + + )} + + )} + + {/* Product Matches Card - hide for payment devices */} + {transponder && matchResult && (matchResult.exactMatches.length > 0 || matchResult.cloneTargets.length > 0) && !transponder.implantName?.includes('Payment Card') && ( COMPATIBLE IMPLANTS - + {getMatchSummary(matchResult, transponder.chipName)} @@ -171,7 +309,7 @@ export function ResultScreen({route, navigation}: ResultScreenProps) { NO DIRECT MATCH - + {getMatchSummary(matchResult, transponder.chipName)} @@ -198,7 +336,7 @@ export function ResultScreen({route, navigation}: ResultScreenProps) { SAK SWAP DETECTED - + {transponder.sakSwapInfo.swapType?.replace(/_/g, ' ').toUpperCase()} @@ -293,22 +431,25 @@ export function ResultScreen({route, navigation}: ResultScreenProps) { CHIP NOT IDENTIFIED - + Unable to identify this chip type. It may be unsupported or require advanced detection. )} - {/* Conversion Service Card - show when recommended */} - {(matchResult?.conversionRecommended || !transponder) && ( + {/* Conversion Service Card - show for payment devices, or when recommended and not a real implant */} + {(transponder?.implantName?.includes('Payment Card') || + ((matchResult?.conversionRecommended || !transponder) && !transponder?.implantName)) && ( - {!transponder - ? 'Unknown chip? Our conversion service can help.' - : matchResult?.isCloneable === false - ? "This chip uses cryptographic protection and can't be cloned. Our conversion service can help find alternatives." - : "No direct product match found. Our conversion service can help."} + {transponder?.implantName?.includes('Payment Card') + ? "Payment cards can't be converted to implants due to security restrictions. Our conversion service can help you find an implant that works with your use case." + : !transponder + ? 'Unknown chip? Our conversion service can help.' + : matchResult?.isCloneable === false + ? "This chip uses cryptographic protection and can't be cloned. Our conversion service can help find alternatives." + : "No direct product match found. Our conversion service can help."} )} @@ -124,34 +164,41 @@ export function ScanScreen({navigation}: ScanScreenProps) { {state === 'scanning' && ( <> - - - - + SCANNING {getScanInstructions()} + {scanProgress && scanProgress.current > 1 && ( + + {scanProgress.step} + + )} )} {state === 'error' && ( <> + + ! + - SCAN FAILED + {error?.type === 'TAG_LOST' + ? 'TAG LOST' + : error?.type === 'TIMEOUT' + ? 'TIMEOUT' + : error?.type === 'PERMISSION_DENIED' + ? 'PERMISSION DENIED' + : 'SCAN FAILED'} {error?.message || 'Unable to read NFC tag'} - {error?.type === 'TAG_LOST' && ( + {getErrorHint() && ( - Hold the tag steady until the scan completes + {getErrorHint()} )}