diff --git a/package-lock.json b/package-lock.json index 506aa3d..84490cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "expo-status-bar": "^3.0.9", "react": "19.1.0", "react-native": "0.81.5", - "react-native-dt-theme": "file:react-native-dt-theme-0.2.0.tgz", + "react-native-dt-theme": "file:react-native-dt-theme-0.2.2.tgz", "react-native-nfc-manager": "^3.17.2", "react-native-paper": "^5.14.5", "react-native-safe-area-context": "~5.6.0", @@ -11587,9 +11587,9 @@ } }, "node_modules/react-native-dt-theme": { - "version": "0.2.0", - "resolved": "file:react-native-dt-theme-0.2.0.tgz", - "integrity": "sha512-hp+Pvezv7nzjW9guy50Dz1XFDU2/mFcxQPztBbZTFLwIJyqFspch9Czu/ACHrKw37ZDGop9mVnOTuJP+eP1aJw==", + "version": "0.2.2", + "resolved": "file:react-native-dt-theme-0.2.2.tgz", + "integrity": "sha512-NGBOguPGg/48t7h7xICngHiUzCElUN1sK6/CS2twB/mlkzO9p1xC7SEnElBShOZcfRsRd7p0vWqeJsPY2q4MXQ==", "license": "MIT", "peerDependencies": { "expo-font": ">=11.0.0", diff --git a/package.json b/package.json index 1450d48..2a2663e 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "expo-status-bar": "^3.0.9", "react": "19.1.0", "react-native": "0.81.5", - "react-native-dt-theme": "file:react-native-dt-theme-0.2.0.tgz", + "react-native-dt-theme": "file:react-native-dt-theme-0.2.2.tgz", "react-native-nfc-manager": "^3.17.2", "react-native-paper": "^5.14.5", "react-native-safe-area-context": "~5.6.0", diff --git a/src/data/products.ts b/src/data/products.ts index 781ff81..a1a9ea4 100644 --- a/src/data/products.ts +++ b/src/data/products.ts @@ -97,6 +97,7 @@ export const PRODUCTS: Product[] = [ url: 'https://dangerousthings.com/product/xnt/', canReceiveClone: true, exactMatch: true, + notes: "UID cannot be changed" }, { id: 'xsiid', @@ -115,6 +116,7 @@ export const PRODUCTS: Product[] = [ url: 'https://dangerousthings.com/product/xsiid/', canReceiveClone: true, exactMatch: true, + notes: "UID cannot be changed" }, { id: 'xslx', @@ -131,6 +133,7 @@ export const PRODUCTS: Product[] = [ url: 'https://dangerousthings.com/product/xslx/', canReceiveClone: true, exactMatch: true, + notes: "UID cannot be changed" }, // ========================================================================== @@ -155,7 +158,7 @@ export const PRODUCTS: Product[] = [ url: 'https://dangerousthings.com/product/next/', canReceiveClone: true, exactMatch: true, - notes: 'Also includes T5577 for 125kHz access systems', + notes: 'HF UID cannot be changed. Also includes T5577 for 125kHz access systems', }, { id: 'next-v2', @@ -176,7 +179,7 @@ export const PRODUCTS: Product[] = [ url: 'https://dangerousthings.com/product/next-v2/', canReceiveClone: true, exactMatch: true, - notes: 'Also includes T5577 for 125kHz access systems', + notes: 'HF UID cannot be changed. Also includes T5577 for 125kHz access systems', }, { id: 'xmagic', @@ -219,7 +222,7 @@ export const PRODUCTS: Product[] = [ url: 'https://dangerousthings.com/product/xdf3/', canReceiveClone: false, exactMatch: true, - notes: 'Cannot clone data due to cryptographic protection', + notes: 'UID cannot be changed. Cannot clone data due to cryptographic protection', desfireEvLevel: 3, }, @@ -262,7 +265,7 @@ export const PRODUCTS: Product[] = [ url: 'https://dangerousthings.com/product/vivokey-spark/', canReceiveClone: false, exactMatch: true, - notes: 'Uses cryptographic authentication - cannot clone', + notes: 'UID cannot be changed. Uses cryptographic authentication - cannot clone', }, // ========================================================================== @@ -287,7 +290,7 @@ export const PRODUCTS: Product[] = [ url: 'https://dangerousthings.com/product/dnext/', canReceiveClone: true, exactMatch: true, - notes: 'Larger size provides improved performance over x-series', + notes: 'HF UID cannot be changed. Larger size provides improved performance over x-series', }, { id: 'dug4t', @@ -332,6 +335,7 @@ export const PRODUCTS: Product[] = [ url: 'https://dangerousthings.com/product/flexnt/', canReceiveClone: true, exactMatch: true, + notes: "UID cannot be changed" }, // ========================================================================== @@ -354,7 +358,7 @@ export const PRODUCTS: Product[] = [ url: 'https://dangerousthings.com/product/flexdf2/', canReceiveClone: false, exactMatch: true, - notes: 'Cannot clone data due to cryptographic protection', + notes: 'UID cannot be changed. Cannot clone data due to cryptographic protection', desfireEvLevel: 2, }, @@ -400,7 +404,7 @@ export const PRODUCTS: Product[] = [ url: 'https://dangerousthings.com/product/apex-flex/', canReceiveClone: false, exactMatch: true, - notes: 'Cannot clone - programmable via Fidesmo app', + notes: 'UID cannot be changed. Cannot clone - programmable via Fidesmo app', }, { id: 'flexsecure', @@ -422,7 +426,7 @@ export const PRODUCTS: Product[] = [ url: 'https://dangerousthings.com/product/flexsecure/', canReceiveClone: false, exactMatch: true, - notes: 'Developer-focused - requires technical knowledge', + notes: 'UID cannot be changed. Developer-focused - requires technical knowledge', }, // ========================================================================== diff --git a/src/screens/ResultScreen.tsx b/src/screens/ResultScreen.tsx index 4297669..44d4c3f 100644 --- a/src/screens/ResultScreen.tsx +++ b/src/screens/ResultScreen.tsx @@ -1,7 +1,43 @@ -import React, { useMemo, useState } from 'react'; -import { StyleSheet, View, ScrollView, Linking, TouchableOpacity } from 'react-native'; +import React, { useMemo, useState, useEffect, useRef } from 'react'; +import { StyleSheet, View, ScrollView, Linking, TouchableOpacity, Animated } from 'react-native'; import { Button, Text, Surface, Divider, Chip } from 'react-native-paper'; import { DTCard, DTButton, DTColors, DTLabel, DTChip } from 'react-native-dt-theme'; + +// Animated section wrapper for staggered entry +interface AnimatedSectionProps { + children: React.ReactNode; + delay: number; + duration?: number; + style?: any; +} + +function AnimatedSection({ children, delay, duration = 400, style }: AnimatedSectionProps) { + const opacity = useRef(new Animated.Value(0)).current; + const translateY = useRef(new Animated.Value(20)).current; + + useEffect(() => { + Animated.parallel([ + Animated.timing(opacity, { + toValue: 1, + duration, + delay, + useNativeDriver: true, + }), + Animated.timing(translateY, { + toValue: 0, + duration, + delay, + useNativeDriver: true, + }), + ]).start(); + }, [delay, duration, opacity, translateY]); + + return ( + + {children} + + ); +} import type { ResultScreenProps } from '../types/navigation'; import { matchChipToProducts, getMatchSummary, getDesfireEvMismatchWarning, getMifareClassicCapacityWarning } from '../services/matching'; import { Product } from '../types/products'; @@ -29,6 +65,36 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) { return getChipFamilyInfo(getChipFamily(transponder.type)); }, [transponder]); + // Calculate product count for animation timing + const productCount = useMemo(() => { + if (!matchResult) return 0; + const exactCount = matchResult.exactMatches.length; + const cloneCount = matchResult.cloneTargets.filter( + p => !matchResult.exactMatches.find(e => e.id === p.id) + ).length; + return exactCount + cloneCount; + }, [matchResult]); + + // Animation delays - sequential flow + const delays = useMemo(() => { + const base = { + chipIdentified: 0, + aboutChip: 150, + compatibleLabel: 300, + productBase: 450, + productIncrement: 150, + }; + const afterProducts = base.productBase + (productCount * base.productIncrement); + return { + ...base, + noMatch: base.compatibleLabel, // Shows in place of products + sakSwap: base.aboutChip, // Shows after chip info + rawData: productCount > 0 ? afterProducts : 300, + actions: productCount > 0 ? afterProducts + 150 : 450, + conversion: productCount > 0 ? afterProducts + 75 : 375, // Between raw data and actions + }; + }, [productCount]); + // Build URL with UTM tracking parameters for analytics const buildTrackedUrl = (baseUrl: string, content?: string) => { const url = new URL(baseUrl); @@ -72,83 +138,87 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) { {/* Chip Identification Card */} {transponder && ( - + + - - {transponder.chipName} - + + {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() && ( - - {getConfidenceLabel()!.label} - + {/* 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() && ( + + > + {getConfidenceLabel()!.label} + + )} + + + {transponder.memorySize && ( + + + MEMORY + + + {transponder.memorySize} bytes + + )} - - {transponder.memorySize && ( - MEMORY + CLONEABLE - - {transponder.memorySize} bytes + + {transponder.isCloneable ? 'YES' : 'NO'} - )} - - - CLONEABLE - - - {transponder.isCloneable ? 'YES' : 'NO'} - - - - {transponder.cloneabilityNote && ( - - {transponder.cloneabilityNote} - - )} - + {transponder.cloneabilityNote && ( + + {transponder.cloneabilityNote} + + )} + + )} {/* Educational Chip Info Card */} {transponder && (chipInfo || chipFamilyInfo) && ( - + + setShowChipInfo(!showChipInfo)} style={styles.chipInfoHeader} @@ -245,28 +315,23 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) { )} + )} {/* 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)} - */} - - {/* Exact Matches / Clone Targets */} - {[...matchResult.exactMatches, ...matchResult.cloneTargets.filter( - p => !matchResult.exactMatches.find(e => e.id === p.id) - )].map(product => ( - // - + {/* Individual Product Cards - staggered */} + {transponder && matchResult && (matchResult.exactMatches.length > 0 || matchResult.cloneTargets.length > 0) && !transponder.implantName?.includes('Payment Card') && ( + [...matchResult.exactMatches, ...matchResult.cloneTargets.filter( + p => !matchResult.exactMatches.find(e => e.id === p.id) + )].map((product, index) => ( + + {/* {product.name} @@ -311,24 +376,20 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) { handleProductLink(product)} - style={styles.productButton} - labelStyle={styles.productButtonLabel} - compact> + > VIEW PRODUCT - - ))} - {/* */} - + + )) )} {/* No Match Card - show when chip detected but no products match */} {transponder && matchResult && matchResult.exactMatches.length === 0 && matchResult.cloneTargets.length === 0 && ( - + + NO DIRECT MATCH @@ -351,11 +412,13 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) { )} + )} {/* SAK Swap Detection Card */} {transponder?.sakSwapInfo?.hasSakSwap && ( - + + SAK SWAP DETECTED @@ -379,29 +442,34 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) { )} + )} {/* Raw Tag Data Card */} - + + {tagData ? ( <> - + UID - + {tagData.uid || 'N/A'} - + TECHNOLOGIES - - {tagData.techTypes.join(', ') || 'N/A'} - + { + tagData.techTypes.map((t, i) => + + {t} + ) + } {tagData.sak !== undefined && ( @@ -443,10 +511,12 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) { )} + {/* No Detection Card (fallback) */} {!transponder && ( - + + CHIP NOT IDENTIFIED @@ -457,57 +527,60 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) { Need help? Ask on our forum for assistance. - + + )} {/* Conversion Service Card - show for payment devices, or when recommended (but NOT for unidentified chips) */} {transponder && (transponder?.implantName?.includes('Payment Card') || (matchResult?.conversionRecommended && !transponder?.implantName)) && ( - - - {transponder?.implantName?.includes('Payment Card') - ? "Payment cards can't be copied/cloned to implants. Our conversion service might be an option." - : !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 copied/cloned to implants. Our conversion service might be an option." + : !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."} + + + + )} {/* Action Buttons */} - - navigation.navigate('Scan')} - style={{ width: '100%' }}> - SCAN ANOTHER - + + + navigation.navigate('Scan')} + style={{ width: '100%' }}> + SCAN ANOTHER + - - + + + - + ); }