ResultsScreen refinements

This commit is contained in:
michael
2026-01-27 12:16:15 -08:00
parent 83e5e1ceeb
commit 3e9411fc7d
4 changed files with 228 additions and 151 deletions

8
package-lock.json generated
View File

@@ -16,7 +16,7 @@
"expo-status-bar": "^3.0.9", "expo-status-bar": "^3.0.9",
"react": "19.1.0", "react": "19.1.0",
"react-native": "0.81.5", "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-nfc-manager": "^3.17.2",
"react-native-paper": "^5.14.5", "react-native-paper": "^5.14.5",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",
@@ -11587,9 +11587,9 @@
} }
}, },
"node_modules/react-native-dt-theme": { "node_modules/react-native-dt-theme": {
"version": "0.2.0", "version": "0.2.2",
"resolved": "file:react-native-dt-theme-0.2.0.tgz", "resolved": "file:react-native-dt-theme-0.2.2.tgz",
"integrity": "sha512-hp+Pvezv7nzjW9guy50Dz1XFDU2/mFcxQPztBbZTFLwIJyqFspch9Czu/ACHrKw37ZDGop9mVnOTuJP+eP1aJw==", "integrity": "sha512-NGBOguPGg/48t7h7xICngHiUzCElUN1sK6/CS2twB/mlkzO9p1xC7SEnElBShOZcfRsRd7p0vWqeJsPY2q4MXQ==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"expo-font": ">=11.0.0", "expo-font": ">=11.0.0",

View File

@@ -23,7 +23,7 @@
"expo-status-bar": "^3.0.9", "expo-status-bar": "^3.0.9",
"react": "19.1.0", "react": "19.1.0",
"react-native": "0.81.5", "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-nfc-manager": "^3.17.2",
"react-native-paper": "^5.14.5", "react-native-paper": "^5.14.5",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",

View File

@@ -97,6 +97,7 @@ export const PRODUCTS: Product[] = [
url: 'https://dangerousthings.com/product/xnt/', url: 'https://dangerousthings.com/product/xnt/',
canReceiveClone: true, canReceiveClone: true,
exactMatch: true, exactMatch: true,
notes: "UID cannot be changed"
}, },
{ {
id: 'xsiid', id: 'xsiid',
@@ -115,6 +116,7 @@ export const PRODUCTS: Product[] = [
url: 'https://dangerousthings.com/product/xsiid/', url: 'https://dangerousthings.com/product/xsiid/',
canReceiveClone: true, canReceiveClone: true,
exactMatch: true, exactMatch: true,
notes: "UID cannot be changed"
}, },
{ {
id: 'xslx', id: 'xslx',
@@ -131,6 +133,7 @@ export const PRODUCTS: Product[] = [
url: 'https://dangerousthings.com/product/xslx/', url: 'https://dangerousthings.com/product/xslx/',
canReceiveClone: true, canReceiveClone: true,
exactMatch: true, exactMatch: true,
notes: "UID cannot be changed"
}, },
// ========================================================================== // ==========================================================================
@@ -155,7 +158,7 @@ export const PRODUCTS: Product[] = [
url: 'https://dangerousthings.com/product/next/', url: 'https://dangerousthings.com/product/next/',
canReceiveClone: true, canReceiveClone: true,
exactMatch: 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', id: 'next-v2',
@@ -176,7 +179,7 @@ export const PRODUCTS: Product[] = [
url: 'https://dangerousthings.com/product/next-v2/', url: 'https://dangerousthings.com/product/next-v2/',
canReceiveClone: true, canReceiveClone: true,
exactMatch: 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', id: 'xmagic',
@@ -219,7 +222,7 @@ export const PRODUCTS: Product[] = [
url: 'https://dangerousthings.com/product/xdf3/', url: 'https://dangerousthings.com/product/xdf3/',
canReceiveClone: false, canReceiveClone: false,
exactMatch: true, exactMatch: true,
notes: 'Cannot clone data due to cryptographic protection', notes: 'UID cannot be changed. Cannot clone data due to cryptographic protection',
desfireEvLevel: 3, desfireEvLevel: 3,
}, },
@@ -262,7 +265,7 @@ export const PRODUCTS: Product[] = [
url: 'https://dangerousthings.com/product/vivokey-spark/', url: 'https://dangerousthings.com/product/vivokey-spark/',
canReceiveClone: false, canReceiveClone: false,
exactMatch: true, 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/', url: 'https://dangerousthings.com/product/dnext/',
canReceiveClone: true, canReceiveClone: true,
exactMatch: 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', id: 'dug4t',
@@ -332,6 +335,7 @@ export const PRODUCTS: Product[] = [
url: 'https://dangerousthings.com/product/flexnt/', url: 'https://dangerousthings.com/product/flexnt/',
canReceiveClone: true, canReceiveClone: true,
exactMatch: true, exactMatch: true,
notes: "UID cannot be changed"
}, },
// ========================================================================== // ==========================================================================
@@ -354,7 +358,7 @@ export const PRODUCTS: Product[] = [
url: 'https://dangerousthings.com/product/flexdf2/', url: 'https://dangerousthings.com/product/flexdf2/',
canReceiveClone: false, canReceiveClone: false,
exactMatch: true, exactMatch: true,
notes: 'Cannot clone data due to cryptographic protection', notes: 'UID cannot be changed. Cannot clone data due to cryptographic protection',
desfireEvLevel: 2, desfireEvLevel: 2,
}, },
@@ -400,7 +404,7 @@ export const PRODUCTS: Product[] = [
url: 'https://dangerousthings.com/product/apex-flex/', url: 'https://dangerousthings.com/product/apex-flex/',
canReceiveClone: false, canReceiveClone: false,
exactMatch: true, exactMatch: true,
notes: 'Cannot clone - programmable via Fidesmo app', notes: 'UID cannot be changed. Cannot clone - programmable via Fidesmo app',
}, },
{ {
id: 'flexsecure', id: 'flexsecure',
@@ -422,7 +426,7 @@ export const PRODUCTS: Product[] = [
url: 'https://dangerousthings.com/product/flexsecure/', url: 'https://dangerousthings.com/product/flexsecure/',
canReceiveClone: false, canReceiveClone: false,
exactMatch: true, exactMatch: true,
notes: 'Developer-focused - requires technical knowledge', notes: 'UID cannot be changed. Developer-focused - requires technical knowledge',
}, },
// ========================================================================== // ==========================================================================

View File

@@ -1,7 +1,43 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState, useEffect, useRef } from 'react';
import { StyleSheet, View, ScrollView, Linking, TouchableOpacity } from 'react-native'; import { StyleSheet, View, ScrollView, Linking, TouchableOpacity, Animated } from 'react-native';
import { Button, Text, Surface, Divider, Chip } from 'react-native-paper'; import { Button, Text, Surface, Divider, Chip } from 'react-native-paper';
import { DTCard, DTButton, DTColors, DTLabel, DTChip } from 'react-native-dt-theme'; 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 (
<Animated.View style={[{ opacity, transform: [{ translateY }] }, style]}>
{children}
</Animated.View>
);
}
import type { ResultScreenProps } from '../types/navigation'; import type { ResultScreenProps } from '../types/navigation';
import { matchChipToProducts, getMatchSummary, getDesfireEvMismatchWarning, getMifareClassicCapacityWarning } from '../services/matching'; import { matchChipToProducts, getMatchSummary, getDesfireEvMismatchWarning, getMifareClassicCapacityWarning } from '../services/matching';
import { Product } from '../types/products'; import { Product } from '../types/products';
@@ -29,6 +65,36 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
return getChipFamilyInfo(getChipFamily(transponder.type)); return getChipFamilyInfo(getChipFamily(transponder.type));
}, [transponder]); }, [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 // Build URL with UTM tracking parameters for analytics
const buildTrackedUrl = (baseUrl: string, content?: string) => { const buildTrackedUrl = (baseUrl: string, content?: string) => {
const url = new URL(baseUrl); const url = new URL(baseUrl);
@@ -72,6 +138,7 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
<View style={styles.content}> <View style={styles.content}>
{/* Chip Identification Card */} {/* Chip Identification Card */}
{transponder && ( {transponder && (
<AnimatedSection delay={delays.chipIdentified}>
<DTCard mode="success" title="CHIP IDENTIFIED" style={{ marginBottom: 20 }}> <DTCard mode="success" title="CHIP IDENTIFIED" style={{ marginBottom: 20 }}>
<Text variant="headlineMedium" style={styles.chipName}> <Text variant="headlineMedium" style={styles.chipName}>
@@ -102,17 +169,18 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
)} )}
<View style={styles.chipMeta}> <View style={styles.chipMeta}>
<Chip <DTChip
style={[styles.familyChip, { borderColor: DTColors.modeNormal }]} variant="normal">
textStyle={styles.familyChipText}>
{transponder.family} {transponder.family}
</Chip> </DTChip>
{getConfidenceLabel() && ( {getConfidenceLabel() && (
<Chip <DTChip
style={[styles.confidenceChip, { borderColor: getConfidenceLabel()!.color }]} variant='other'
textStyle={[styles.confidenceChipText, { color: getConfidenceLabel()!.color }]}> // style={[styles.confidenceChip, { borderColor: getConfidenceLabel()!.color }]}
// textStyle={[styles.confidenceChipText, { color: getConfidenceLabel()!.color }]}>
>
{getConfidenceLabel()!.label} {getConfidenceLabel()!.label}
</Chip> </DTChip>
)} )}
</View> </View>
@@ -144,10 +212,12 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
</Text> </Text>
)} )}
</DTCard> </DTCard>
</AnimatedSection>
)} )}
{/* Educational Chip Info Card */} {/* Educational Chip Info Card */}
{transponder && (chipInfo || chipFamilyInfo) && ( {transponder && (chipInfo || chipFamilyInfo) && (
<AnimatedSection delay={delays.aboutChip}>
<Surface style={styles.chipInfoCard} elevation={1}> <Surface style={styles.chipInfoCard} elevation={1}>
<TouchableOpacity <TouchableOpacity
onPress={() => setShowChipInfo(!showChipInfo)} onPress={() => setShowChipInfo(!showChipInfo)}
@@ -245,28 +315,23 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
</> </>
)} )}
</Surface> </Surface>
</AnimatedSection>
)} )}
{/* Product Matches Card - hide for payment devices */} {/* Product Matches Card - hide for payment devices */}
{transponder && matchResult && (matchResult.exactMatches.length > 0 || matchResult.cloneTargets.length > 0) && !transponder.implantName?.includes('Payment Card') && ( {transponder && matchResult && (matchResult.exactMatches.length > 0 || matchResult.cloneTargets.length > 0) && !transponder.implantName?.includes('Payment Card') && (
// <Surface style={styles.productsCard} elevation={1}> <AnimatedSection delay={delays.compatibleLabel}>
<> <DTLabel mode='emphasis' primaryText="Compatible Implants" style={{ marginBottom: 25, }} size="large" animated={false} />
{/* <Text variant="labelLarge" style={styles.productsLabel}> </AnimatedSection>
COMPATIBLE IMPLANTS )}
</Text> */}
<DTLabel mode='emphasis' primaryText="Compatible Implants" />
{/* <Divider style={[styles.divider, { backgroundColor: DTColors.modeEmphasis }]} /> */}
{/* <Text variant="bodyMedium" style={styles.matchSummary}> {/* Individual Product Cards - staggered */}
{getMatchSummary(matchResult, transponder.chipName)} {transponder && matchResult && (matchResult.exactMatches.length > 0 || matchResult.cloneTargets.length > 0) && !transponder.implantName?.includes('Payment Card') && (
</Text> */} [...matchResult.exactMatches, ...matchResult.cloneTargets.filter(
{/* Exact Matches / Clone Targets */}
{[...matchResult.exactMatches, ...matchResult.cloneTargets.filter(
p => !matchResult.exactMatches.find(e => e.id === p.id) p => !matchResult.exactMatches.find(e => e.id === p.id)
)].map(product => ( )].map((product, index) => (
// <Surface key={product.id} style={styles.productItem} elevation={0}> <AnimatedSection key={product.id} delay={delays.productBase + (index * delays.productIncrement)}>
<DTCard mode='emphasis' title={product.name} style={{ paddingBottom: 10 }}> <DTCard mode='emphasis' title={product.name} style={{ marginBottom: 25 }}>
<View style={styles.productHeader}> <View style={styles.productHeader}>
{/* <Text variant="titleMedium" style={styles.productName}> {/* <Text variant="titleMedium" style={styles.productName}>
{product.name} {product.name}
@@ -311,23 +376,19 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
</View> </View>
<DTButton <DTButton
mode="outlined"
onPress={() => handleProductLink(product)} onPress={() => handleProductLink(product)}
style={styles.productButton} >
labelStyle={styles.productButtonLabel}
compact>
VIEW PRODUCT VIEW PRODUCT
</DTButton> </DTButton>
</DTCard> </DTCard>
</AnimatedSection>
))} ))
{/* </Surface> */}
</>
)} )}
{/* No Match Card - show when chip detected but no products match */} {/* No Match Card - show when chip detected but no products match */}
{transponder && matchResult && matchResult.exactMatches.length === 0 && matchResult.cloneTargets.length === 0 && ( {transponder && matchResult && matchResult.exactMatches.length === 0 && matchResult.cloneTargets.length === 0 && (
<AnimatedSection delay={delays.noMatch}>
<Surface style={styles.noMatchCard} elevation={1}> <Surface style={styles.noMatchCard} elevation={1}>
<Text variant="labelLarge" style={styles.noMatchLabel}> <Text variant="labelLarge" style={styles.noMatchLabel}>
NO DIRECT MATCH NO DIRECT MATCH
@@ -351,10 +412,12 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
</> </>
)} )}
</Surface> </Surface>
</AnimatedSection>
)} )}
{/* SAK Swap Detection Card */} {/* SAK Swap Detection Card */}
{transponder?.sakSwapInfo?.hasSakSwap && ( {transponder?.sakSwapInfo?.hasSakSwap && (
<AnimatedSection delay={delays.sakSwap}>
<Surface style={styles.sakSwapCard} elevation={1}> <Surface style={styles.sakSwapCard} elevation={1}>
<Text variant="labelLarge" style={styles.sakSwapLabel}> <Text variant="labelLarge" style={styles.sakSwapLabel}>
SAK SWAP DETECTED SAK SWAP DETECTED
@@ -379,29 +442,34 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
</View> </View>
)} )}
</Surface> </Surface>
</AnimatedSection>
)} )}
{/* Raw Tag Data Card */} {/* Raw Tag Data Card */}
<AnimatedSection delay={delays.rawData}>
<DTCard mode="normal" title="RAW TAG DATA" style={{ marginBottom: 20 }}> <DTCard mode="normal" title="RAW TAG DATA" style={{ marginBottom: 20 }}>
{tagData ? ( {tagData ? (
<> <>
<View style={styles.dataRow}> <View style={styles.dataRow}>
<Text variant="bodyMedium" style={styles.dataLabel}> <Text variant="bodyLarge" style={styles.dataLabel}>
UID UID
</Text> </Text>
<Text variant="bodyLarge" style={styles.dataValue}> <Text variant="bodyMedium" style={styles.dataValue}>
{tagData.uid || 'N/A'} {tagData.uid || 'N/A'}
</Text> </Text>
</View> </View>
<View style={styles.dataRow}> <View style={styles.dataRow}>
<Text variant="bodyMedium" style={styles.dataLabel}> <Text variant="bodyLarge" style={styles.dataLabel}>
TECHNOLOGIES TECHNOLOGIES
</Text> </Text>
<Text variant="bodyLarge" style={styles.dataValue}> {
{tagData.techTypes.join(', ') || 'N/A'} tagData.techTypes.map((t, i) =>
</Text> <Text key={i} variant="bodyMedium" style={styles.dataValue}>
{t}
</Text>)
}
</View> </View>
{tagData.sak !== undefined && ( {tagData.sak !== undefined && (
@@ -443,9 +511,11 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
</Text> </Text>
)} )}
</DTCard> </DTCard>
</AnimatedSection>
{/* No Detection Card (fallback) */} {/* No Detection Card (fallback) */}
{!transponder && ( {!transponder && (
<AnimatedSection delay={0}>
<Surface style={styles.noDetectionCard} elevation={1}> <Surface style={styles.noDetectionCard} elevation={1}>
<Text variant="labelLarge" style={styles.noDetectionLabel}> <Text variant="labelLarge" style={styles.noDetectionLabel}>
CHIP NOT IDENTIFIED CHIP NOT IDENTIFIED
@@ -457,19 +527,19 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
<Text variant="bodyMedium" style={styles.noDetectionText}> <Text variant="bodyMedium" style={styles.noDetectionText}>
Need help? Ask on our forum for assistance. Need help? Ask on our forum for assistance.
</Text> </Text>
<Button <DTButton
mode="outlined"
onPress={() => Linking.openURL(buildTrackedUrl('https://dngr.us/forum', 'unknown_chip'))} onPress={() => Linking.openURL(buildTrackedUrl('https://dngr.us/forum', 'unknown_chip'))}
style={styles.forumButton} >
labelStyle={styles.forumButtonLabel}>
VISIT FORUM VISIT FORUM
</Button> </DTButton>
</Surface> </Surface>
</AnimatedSection>
)} )}
{/* Conversion Service Card - show for payment devices, or when recommended (but NOT for unidentified chips) */} {/* Conversion Service Card - show for payment devices, or when recommended (but NOT for unidentified chips) */}
{transponder && (transponder?.implantName?.includes('Payment Card') || {transponder && (transponder?.implantName?.includes('Payment Card') ||
(matchResult?.conversionRecommended && !transponder?.implantName)) && ( (matchResult?.conversionRecommended && !transponder?.implantName)) && (
<AnimatedSection delay={delays.conversion}>
<Surface style={styles.conversionCard} elevation={1}> <Surface style={styles.conversionCard} elevation={1}>
<Text variant="bodyMedium" style={styles.conversionText}> <Text variant="bodyMedium" style={styles.conversionText}>
{transponder?.implantName?.includes('Payment Card') {transponder?.implantName?.includes('Payment Card')
@@ -488,9 +558,11 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
CONVERSION SERVICE CONVERSION SERVICE
</Button> </Button>
</Surface> </Surface>
</AnimatedSection>
)} )}
{/* Action Buttons */} {/* Action Buttons */}
<AnimatedSection delay={delays.actions}>
<View style={styles.actions}> <View style={styles.actions}>
<DTButton <DTButton
variant="normal" variant="normal"
@@ -506,8 +578,9 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
HOME HOME
</Button> </Button>
</View> </View>
</AnimatedSection>
</View> </View>
</ScrollView> </ScrollView >
); );
} }