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",
"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",

View File

@@ -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",

View File

@@ -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',
},
// ==========================================================================

View File

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