ResultsScreen refinements
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
// ==========================================================================
|
||||
|
||||
@@ -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 >
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user