Fixed UID length bug for product suggestions re: MFC and payment verbiage
This commit is contained in:
3
app.json
3
app.json
@@ -14,7 +14,8 @@
|
|||||||
"supportsTablet": false,
|
"supportsTablet": false,
|
||||||
"bundleIdentifier": "com.dangerousthings.nfcidentifier",
|
"bundleIdentifier": "com.dangerousthings.nfcidentifier",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"NFCReaderUsageDescription": "This app uses NFC to scan transponders and identify compatible Dangerous Things implants."
|
"NFCReaderUsageDescription": "This app uses NFC to scan transponders and identify compatible Dangerous Things implants.",
|
||||||
|
"ITSAppUsesNonExemptEncryption": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
|
|||||||
6
eas.json
6
eas.json
@@ -11,6 +11,12 @@
|
|||||||
"preview": {
|
"preview": {
|
||||||
"distribution": "internal"
|
"distribution": "internal"
|
||||||
},
|
},
|
||||||
|
"preview-apk": {
|
||||||
|
"distribution": "internal",
|
||||||
|
"android": {
|
||||||
|
"buildType": "apk"
|
||||||
|
}
|
||||||
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"autoIncrement": true
|
"autoIncrement": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* Discontinued products removed per https://forum.dangerousthings.com/t/shes-dead-jim/26308
|
* Discontinued products removed per https://forum.dangerousthings.com/t/shes-dead-jim/26308
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ChipType} from '../types/detection';
|
import { ChipType } from '../types/detection';
|
||||||
import {
|
import {
|
||||||
Product,
|
Product,
|
||||||
FormFactor,
|
FormFactor,
|
||||||
@@ -72,7 +72,7 @@ export const PRODUCTS: Product[] = [
|
|||||||
id: 'xnt',
|
id: 'xnt',
|
||||||
name: 'xNT',
|
name: 'xNT',
|
||||||
description:
|
description:
|
||||||
'NFC Type 2 implant with 888 bytes of user memory. The original and most popular NFC implant.',
|
'NFC Type 2 implant with 888 bytes of user memory.',
|
||||||
formFactor: FormFactor.X_SERIES,
|
formFactor: FormFactor.X_SERIES,
|
||||||
categories: [ProductCategory.NFC],
|
categories: [ProductCategory.NFC],
|
||||||
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
|
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
|
||||||
@@ -96,9 +96,9 @@ export const PRODUCTS: Product[] = [
|
|||||||
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
|
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
|
||||||
features: [
|
features: [
|
||||||
'NTAG I2C chip',
|
'NTAG I2C chip',
|
||||||
'888 bytes user memory',
|
'1kB user memory',
|
||||||
'Built-in LED indicator',
|
'URL/contact sharing',
|
||||||
'Multiple colors available',
|
'Red, Green, Blue, and White LEDs available',
|
||||||
],
|
],
|
||||||
url: 'https://dangerousthings.com/product/xsiid/',
|
url: 'https://dangerousthings.com/product/xsiid/',
|
||||||
canReceiveClone: true,
|
canReceiveClone: true,
|
||||||
@@ -107,14 +107,14 @@ export const PRODUCTS: Product[] = [
|
|||||||
{
|
{
|
||||||
id: 'xslx',
|
id: 'xslx',
|
||||||
name: 'xSLX',
|
name: 'xSLX',
|
||||||
description: 'ISO 15693 NFC-V implant using ICODE SLIX chip.',
|
description: 'NFC Type 5 implant using ICODE SLIX chip.',
|
||||||
formFactor: FormFactor.X_SERIES,
|
formFactor: FormFactor.X_SERIES,
|
||||||
categories: [ProductCategory.NFC],
|
categories: [ProductCategory.NFC],
|
||||||
compatibleChips: [...SLIX_COMPATIBLE],
|
compatibleChips: [...SLIX_COMPATIBLE],
|
||||||
features: [
|
features: [
|
||||||
'ICODE SLIX chip',
|
'ICODE SLIX chip',
|
||||||
'ISO 15693 (NFC-V)',
|
'320 bbytes user memory',
|
||||||
'Long range capability',
|
'URL/contact sharing',
|
||||||
],
|
],
|
||||||
url: 'https://dangerousthings.com/product/xslx/',
|
url: 'https://dangerousthings.com/product/xslx/',
|
||||||
canReceiveClone: true,
|
canReceiveClone: true,
|
||||||
@@ -133,10 +133,11 @@ export const PRODUCTS: Product[] = [
|
|||||||
categories: [ProductCategory.DUAL_FREQUENCY, ProductCategory.ACCESS],
|
categories: [ProductCategory.DUAL_FREQUENCY, ProductCategory.ACCESS],
|
||||||
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
|
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
|
||||||
features: [
|
features: [
|
||||||
|
'Dual-frequency',
|
||||||
'NTAG216 NFC chip',
|
'NTAG216 NFC chip',
|
||||||
|
'888 bytes user memory',
|
||||||
'T5577 125kHz chip',
|
'T5577 125kHz chip',
|
||||||
'Dual-frequency capability',
|
'LF access control',
|
||||||
'HID/EM compatible',
|
|
||||||
],
|
],
|
||||||
url: 'https://dangerousthings.com/product/next/',
|
url: 'https://dangerousthings.com/product/next/',
|
||||||
canReceiveClone: true,
|
canReceiveClone: true,
|
||||||
@@ -147,15 +148,17 @@ export const PRODUCTS: Product[] = [
|
|||||||
id: 'next-v2',
|
id: 'next-v2',
|
||||||
name: 'NExT v2',
|
name: 'NExT v2',
|
||||||
description:
|
description:
|
||||||
'Updated dual-frequency implant with NTAG I2C, T5577, and built-in LED indicator.',
|
'Updated dual-frequency implant with NTAG I2C, T5577, and built-in LED.',
|
||||||
formFactor: FormFactor.X_SERIES,
|
formFactor: FormFactor.X_SERIES,
|
||||||
categories: [ProductCategory.DUAL_FREQUENCY, ProductCategory.ACCESS, ProductCategory.LED],
|
categories: [ProductCategory.DUAL_FREQUENCY, ProductCategory.ACCESS, ProductCategory.LED],
|
||||||
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
|
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
|
||||||
features: [
|
features: [
|
||||||
'NTAG I2C NFC chip',
|
'NTAG I2C NFC chip',
|
||||||
|
'1kB user memory',
|
||||||
|
'URL/contact sharing',
|
||||||
|
'Green, Blue, and White LEDs available (HF)',
|
||||||
'T5577 125kHz chip',
|
'T5577 125kHz chip',
|
||||||
'Built-in LED indicator',
|
'LF access control',
|
||||||
'888 bytes user memory',
|
|
||||||
],
|
],
|
||||||
url: 'https://dangerousthings.com/product/next-v2/',
|
url: 'https://dangerousthings.com/product/next-v2/',
|
||||||
canReceiveClone: true,
|
canReceiveClone: true,
|
||||||
@@ -240,38 +243,15 @@ export const PRODUCTS: Product[] = [
|
|||||||
categories: [ProductCategory.SECURE],
|
categories: [ProductCategory.SECURE],
|
||||||
compatibleChips: [ChipType.NTAG424_DNA, ChipType.NTAG424_DNA_TT, ChipType.ICODE_DNA],
|
compatibleChips: [ChipType.NTAG424_DNA, ChipType.NTAG424_DNA_TT, ChipType.ICODE_DNA],
|
||||||
features: [
|
features: [
|
||||||
'Cryptographic authentication',
|
'Spark Actions',
|
||||||
'VivoKey identity platform',
|
'Verify API',
|
||||||
'Secure web login',
|
|
||||||
'Digital signatures',
|
|
||||||
],
|
],
|
||||||
url: 'https://dangerousthings.com/product/vivokey-spark-2/',
|
url: 'https://dangerousthings.com/product/vivokey-spark/',
|
||||||
canReceiveClone: false,
|
canReceiveClone: false,
|
||||||
exactMatch: true,
|
exactMatch: true,
|
||||||
notes: 'Uses cryptographic authentication - cannot clone',
|
notes: 'Uses cryptographic authentication - cannot clone',
|
||||||
},
|
},
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// X-Series - LED/Special
|
|
||||||
// ==========================================================================
|
|
||||||
{
|
|
||||||
id: 'xglow',
|
|
||||||
name: 'xGlow',
|
|
||||||
description: 'Implant with LED that glows when exposed to NFC field.',
|
|
||||||
formFactor: FormFactor.X_SERIES,
|
|
||||||
categories: [ProductCategory.LED],
|
|
||||||
compatibleChips: [], // No NFC storage
|
|
||||||
features: [
|
|
||||||
'LED indicator',
|
|
||||||
'Multiple colors',
|
|
||||||
'Field detection',
|
|
||||||
],
|
|
||||||
url: 'https://dangerousthings.com/product/xglow/',
|
|
||||||
canReceiveClone: false,
|
|
||||||
exactMatch: false,
|
|
||||||
notes: 'LED only - no NFC data storage',
|
|
||||||
},
|
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Bioresin Implants - Larger capsules (incision install)
|
// Bioresin Implants - Larger capsules (incision install)
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -313,25 +293,6 @@ export const PRODUCTS: Product[] = [
|
|||||||
exactMatch: false,
|
exactMatch: false,
|
||||||
notes: 'Can clone MIFARE Classic cards (requires Android + keys)',
|
notes: 'Can clone MIFARE Classic cards (requires Android + keys)',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'dt5',
|
|
||||||
name: 'dT5',
|
|
||||||
description:
|
|
||||||
'Bioresin 125kHz implant with T5577 chip for access control systems.',
|
|
||||||
formFactor: FormFactor.BIORESIN,
|
|
||||||
categories: [ProductCategory.ACCESS],
|
|
||||||
compatibleChips: [], // 125kHz only, no NFC
|
|
||||||
features: [
|
|
||||||
'T5577 125kHz chip',
|
|
||||||
'EM41xx emulation',
|
|
||||||
'HID ProxCard compatible',
|
|
||||||
'Bioresin encapsulation',
|
|
||||||
],
|
|
||||||
url: 'https://dangerousthings.com/product/dt5/',
|
|
||||||
canReceiveClone: false,
|
|
||||||
exactMatch: false,
|
|
||||||
notes: '125kHz only - no NFC capability',
|
|
||||||
},
|
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Flex Implants - NFC
|
// Flex Implants - NFC
|
||||||
@@ -482,23 +443,6 @@ export const PRODUCTS: Product[] = [
|
|||||||
exactMatch: false,
|
exactMatch: false,
|
||||||
notes: 'For HID iCLASS systems only',
|
notes: 'For HID iCLASS systems only',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'teslaflex',
|
|
||||||
name: 'TeslaFlex',
|
|
||||||
description: 'Implant designed to work as a Tesla key card.',
|
|
||||||
formFactor: FormFactor.FLEX,
|
|
||||||
categories: [ProductCategory.ACCESS],
|
|
||||||
compatibleChips: [], // Tesla proprietary
|
|
||||||
features: [
|
|
||||||
'Tesla key card compatible',
|
|
||||||
'Flexible form factor',
|
|
||||||
'Direct Tesla integration',
|
|
||||||
],
|
|
||||||
url: 'https://dangerousthings.com/product/teslaflex/',
|
|
||||||
canReceiveClone: false,
|
|
||||||
exactMatch: false,
|
|
||||||
notes: 'Specifically for Tesla vehicles',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'flexug4',
|
id: 'flexug4',
|
||||||
name: 'flexUG4',
|
name: 'flexUG4',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export interface UseScanResult {
|
|||||||
/** Scanned tag data (when state is 'success') */
|
/** Scanned tag data (when state is 'success') */
|
||||||
tag: RawTagData | null;
|
tag: RawTagData | null;
|
||||||
/** Detected transponder info (when detection succeeds) */
|
/** Detected transponder info (when detection succeeds) */
|
||||||
transponder: Transponder | null;
|
transponder: Transponder | null | undefined;
|
||||||
/** Scan error (when state is 'error') */
|
/** Scan error (when state is 'error') */
|
||||||
error: ScanError | null;
|
error: ScanError | null;
|
||||||
/** NFC status (supported and enabled) */
|
/** NFC status (supported and enabled) */
|
||||||
@@ -45,10 +45,19 @@ export interface UseScanResult {
|
|||||||
/**
|
/**
|
||||||
* Hook for managing NFC scanning
|
* Hook for managing NFC scanning
|
||||||
*/
|
*/
|
||||||
|
// Combined scan result to ensure atomic updates
|
||||||
|
interface ScanResult {
|
||||||
|
tag: RawTagData | null;
|
||||||
|
transponder: Transponder | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function useScan(): UseScanResult {
|
export function useScan(): UseScanResult {
|
||||||
const [state, setState] = useState<ScanState>('idle');
|
const [state, setState] = useState<ScanState>('idle');
|
||||||
const [tag, setTag] = useState<RawTagData | null>(null);
|
// Use combined state to ensure tag and transponder update atomically
|
||||||
const [transponder, setTransponder] = useState<Transponder | null>(null);
|
const [scanResult, setScanResult] = useState<ScanResult>({
|
||||||
|
tag: null,
|
||||||
|
transponder: undefined,
|
||||||
|
});
|
||||||
const [error, setError] = useState<ScanError | null>(null);
|
const [error, setError] = useState<ScanError | null>(null);
|
||||||
const [nfcStatus, setNfcStatus] = useState<NFCStatus>({
|
const [nfcStatus, setNfcStatus] = useState<NFCStatus>({
|
||||||
isSupported: false,
|
isSupported: false,
|
||||||
@@ -93,8 +102,7 @@ export function useScan(): UseScanResult {
|
|||||||
|
|
||||||
scanInProgress.current = true;
|
scanInProgress.current = true;
|
||||||
setState('scanning');
|
setState('scanning');
|
||||||
setTag(null);
|
setScanResult({ tag: null, transponder: undefined });
|
||||||
setTransponder(null);
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setScanProgress({step: 'Waiting for tag...', current: 1});
|
setScanProgress({step: 'Waiting for tag...', current: 1});
|
||||||
|
|
||||||
@@ -162,11 +170,18 @@ export function useScan(): UseScanResult {
|
|||||||
setError(result.error);
|
setError(result.error);
|
||||||
}
|
}
|
||||||
} else if (result.tag) {
|
} else if (result.tag) {
|
||||||
|
const detectedTransponder = result.detection ?? null;
|
||||||
|
console.log('[useScan] Scan complete:', {
|
||||||
|
hasTag: !!result.tag,
|
||||||
|
hasDetection: !!result.detection,
|
||||||
|
transponderType: detectedTransponder?.type,
|
||||||
|
});
|
||||||
|
// Update tag and transponder atomically in a single state update
|
||||||
|
setScanResult({
|
||||||
|
tag: result.tag,
|
||||||
|
transponder: detectedTransponder,
|
||||||
|
});
|
||||||
setState('success');
|
setState('success');
|
||||||
setTag(result.tag);
|
|
||||||
if (result.detection) {
|
|
||||||
setTransponder(result.detection);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setState('error');
|
setState('error');
|
||||||
setError({
|
setError({
|
||||||
@@ -201,8 +216,7 @@ export function useScan(): UseScanResult {
|
|||||||
// Reset state to idle
|
// Reset state to idle
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
setState('idle');
|
setState('idle');
|
||||||
setTag(null);
|
setScanResult({ tag: null, transponder: undefined });
|
||||||
setTransponder(null);
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setScanProgress(null);
|
setScanProgress(null);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -219,8 +233,8 @@ export function useScan(): UseScanResult {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
tag,
|
tag: scanResult.tag,
|
||||||
transponder,
|
transponder: scanResult.transponder,
|
||||||
error,
|
error,
|
||||||
nfcStatus,
|
nfcStatus,
|
||||||
scanProgress,
|
scanProgress,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
|
|||||||
// Match chip to products
|
// Match chip to products
|
||||||
const matchResult = useMemo(() => {
|
const matchResult = useMemo(() => {
|
||||||
if (!transponder) return null;
|
if (!transponder) return null;
|
||||||
return matchChipToProducts(transponder.type);
|
return matchChipToProducts(transponder);
|
||||||
}, [transponder]);
|
}, [transponder]);
|
||||||
|
|
||||||
// Get educational chip info
|
// Get educational chip info
|
||||||
@@ -449,32 +449,42 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
|
|||||||
<Text variant="bodyMedium" style={styles.noDetectionText}>
|
<Text variant="bodyMedium" style={styles.noDetectionText}>
|
||||||
Unable to identify this chip type. It may be unsupported or require advanced detection.
|
Unable to identify this chip type. It may be unsupported or require advanced detection.
|
||||||
</Text>
|
</Text>
|
||||||
</Surface>
|
<Text variant="bodyMedium" style={styles.noDetectionText}>
|
||||||
)}
|
Need help? Ask on our forum for assistance.
|
||||||
|
|
||||||
{/* 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)) && (
|
|
||||||
<Surface style={styles.conversionCard} elevation={1}>
|
|
||||||
<Text variant="bodyMedium" style={styles.conversionText}>
|
|
||||||
{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."}
|
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
mode="outlined"
|
mode="outlined"
|
||||||
onPress={handleConversionLink}
|
onPress={() => Linking.openURL(buildTrackedUrl('https://dngr.us/forum', 'unknown_chip'))}
|
||||||
style={styles.conversionButton}
|
style={styles.forumButton}
|
||||||
labelStyle={styles.conversionButtonLabel}>
|
labelStyle={styles.forumButtonLabel}>
|
||||||
CONVERSION SERVICE
|
VISIT FORUM
|
||||||
</Button>
|
</Button>
|
||||||
</Surface>
|
</Surface>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 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)) && (
|
||||||
|
<Surface style={styles.conversionCard} elevation={1}>
|
||||||
|
<Text variant="bodyMedium" style={styles.conversionText}>
|
||||||
|
{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."}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
mode="outlined"
|
||||||
|
onPress={handleConversionLink}
|
||||||
|
style={styles.conversionButton}
|
||||||
|
labelStyle={styles.conversionButtonLabel}>
|
||||||
|
CONVERSION SERVICE
|
||||||
|
</Button>
|
||||||
|
</Surface>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
<Button
|
<Button
|
||||||
@@ -650,6 +660,16 @@ const styles = StyleSheet.create({
|
|||||||
noDetectionText: {
|
noDetectionText: {
|
||||||
color: DTColors.light,
|
color: DTColors.light,
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
forumButton: {
|
||||||
|
borderColor: DTColors.modeWarning,
|
||||||
|
borderWidth: 1,
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
|
forumButtonLabel: {
|
||||||
|
color: DTColors.modeWarning,
|
||||||
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
// Products Card
|
// Products Card
|
||||||
productsCard: {
|
productsCard: {
|
||||||
|
|||||||
@@ -20,9 +20,25 @@ export function ScanScreen({navigation}: ScanScreenProps) {
|
|||||||
openSettings,
|
openSettings,
|
||||||
} = useScan();
|
} = useScan();
|
||||||
|
|
||||||
// Navigate to results when scan succeeds (detection happens in the hook)
|
// Track if we've already navigated to prevent double navigation
|
||||||
|
const hasNavigated = React.useRef(false);
|
||||||
|
|
||||||
|
// Navigate to results when scan succeeds AND transponder is set
|
||||||
|
// We need transponder to be set (even if null for failed detection) before navigating
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state === 'success' && tag) {
|
// Only navigate once per scan session
|
||||||
|
if (hasNavigated.current) return;
|
||||||
|
|
||||||
|
// Wait for success state, tag data, AND transponder to be set
|
||||||
|
// transponder: undefined = not yet set, null = detection failed, Transponder = success
|
||||||
|
if (state === 'success' && tag && transponder !== undefined) {
|
||||||
|
hasNavigated.current = true;
|
||||||
|
console.log('[ScanScreen] Navigating to Result:', {
|
||||||
|
hasTag: !!tag,
|
||||||
|
hasTransponder: !!transponder,
|
||||||
|
transponderType: transponder?.type,
|
||||||
|
transponderChipName: transponder?.chipName,
|
||||||
|
});
|
||||||
navigation.replace('Result', {
|
navigation.replace('Result', {
|
||||||
tagData: {
|
tagData: {
|
||||||
uid: tag.uid,
|
uid: tag.uid,
|
||||||
@@ -37,6 +53,13 @@ export function ScanScreen({navigation}: ScanScreenProps) {
|
|||||||
}
|
}
|
||||||
}, [state, tag, transponder, navigation]);
|
}, [state, tag, transponder, navigation]);
|
||||||
|
|
||||||
|
// Reset navigation flag when starting a new scan
|
||||||
|
useEffect(() => {
|
||||||
|
if (state === 'scanning') {
|
||||||
|
hasNavigated.current = false;
|
||||||
|
}
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
// Auto-start scan when screen loads
|
// Auto-start scan when screen loads
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Small delay to ensure screen is mounted
|
// Small delay to ensure screen is mounted
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {ChipType, DesfireVersionInfo} from '../../types/detection';
|
import {ChipType, DesfireVersionInfo} from '../../types/detection';
|
||||||
|
import type {NdefRecord} from '../../types/nfc';
|
||||||
import {
|
import {
|
||||||
DESFIRE_GET_VERSION,
|
DESFIRE_GET_VERSION,
|
||||||
DESFIRE_GET_VERSION_CONTINUE,
|
DESFIRE_GET_VERSION_CONTINUE,
|
||||||
@@ -14,11 +15,73 @@ import {
|
|||||||
KNOWN_AIDS,
|
KNOWN_AIDS,
|
||||||
} from '../nfc/commands';
|
} from '../nfc/commands';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URI record type name format prefixes
|
||||||
|
* https://www.nfc-forum.org/specs/
|
||||||
|
*/
|
||||||
|
const URI_PREFIXES: Record<number, string> = {
|
||||||
|
0x00: '',
|
||||||
|
0x01: 'http://www.',
|
||||||
|
0x02: 'https://www.',
|
||||||
|
0x03: 'http://',
|
||||||
|
0x04: 'https://',
|
||||||
|
0x05: 'tel:',
|
||||||
|
0x06: 'mailto:',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect Spark 2 implant from cached NDEF records
|
||||||
|
* This should be called BEFORE any APDU commands to avoid putting the tag
|
||||||
|
* into native mode which breaks ISO 7816-4 NDEF reading
|
||||||
|
*/
|
||||||
|
export function detectSpark2FromNdef(
|
||||||
|
ndefRecords?: NdefRecord[],
|
||||||
|
): Spark2DetectionResult {
|
||||||
|
if (!ndefRecords || ndefRecords.length === 0) {
|
||||||
|
return {found: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DESFire] Checking cached NDEF records for Spark 2...');
|
||||||
|
|
||||||
|
for (const record of ndefRecords) {
|
||||||
|
// Check for URI record (TNF=1, type="U")
|
||||||
|
if (record.tnf === 1 && record.type === 'U' && record.payload.length > 0) {
|
||||||
|
// First byte is URI prefix code
|
||||||
|
const prefixCode = record.payload[0];
|
||||||
|
const prefix = URI_PREFIXES[prefixCode] || '';
|
||||||
|
const uriBody = record.payload
|
||||||
|
.slice(1)
|
||||||
|
.map(b => String.fromCharCode(b))
|
||||||
|
.join('');
|
||||||
|
const fullUri = prefix + uriBody;
|
||||||
|
|
||||||
|
console.log('[DESFire] Found URI record:', fullUri);
|
||||||
|
|
||||||
|
// Check for vivokey.co pattern
|
||||||
|
const vivokeyMatch = fullUri.match(/vivokey\.co\/([A-Za-z0-9_\-]+)/i);
|
||||||
|
if (vivokeyMatch) {
|
||||||
|
const code = vivokeyMatch[1];
|
||||||
|
const url = `https://vivokey.co/${code}`;
|
||||||
|
console.log('[DESFire] Found Spark 2 URL:', url);
|
||||||
|
return {
|
||||||
|
found: true,
|
||||||
|
name: 'Spark 2',
|
||||||
|
url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DESFire] No vivokey.co URL in NDEF records');
|
||||||
|
return {found: false};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Product type values from GET_VERSION byte 1
|
* Product type values from GET_VERSION byte 1
|
||||||
*/
|
*/
|
||||||
const PRODUCT_TYPES = {
|
const PRODUCT_TYPES = {
|
||||||
DESFIRE: 0x01, // Standard DESFire
|
DESFIRE: 0x01, // Standard DESFire
|
||||||
|
NTAG: 0x04, // NTAG family (including NTAG 413 DNA which uses 0x04)
|
||||||
NTAG_I2C: 0x05, // NTAG I2C family (can have ISO-DEP on Plus variants)
|
NTAG_I2C: 0x05, // NTAG I2C family (can have ISO-DEP on Plus variants)
|
||||||
DESFIRE_LIGHT: 0x08, // DESFire Light
|
DESFIRE_LIGHT: 0x08, // DESFire Light
|
||||||
NTAG_424_DNA: 0x21, // NTAG 424 DNA family
|
NTAG_424_DNA: 0x21, // NTAG 424 DNA family
|
||||||
@@ -184,7 +247,7 @@ export async function detectDesfire(): Promise<DesfireDetectionResult> {
|
|||||||
let chipType: ChipType;
|
let chipType: ChipType;
|
||||||
|
|
||||||
if (hwProductType === PRODUCT_TYPES.NTAG_424_DNA) {
|
if (hwProductType === PRODUCT_TYPES.NTAG_424_DNA) {
|
||||||
// NTAG DNA family (413 DNA and 424 DNA share product type 0x21)
|
// NTAG 424 DNA family uses product type 0x21
|
||||||
// Subtype 0x02 = TagTamper variant (424 DNA TT only)
|
// Subtype 0x02 = TagTamper variant (424 DNA TT only)
|
||||||
// Storage size helps differentiate:
|
// Storage size helps differentiate:
|
||||||
// - NTAG 413 DNA: ~160 bytes user memory (storage code <= 0x0E)
|
// - NTAG 413 DNA: ~160 bytes user memory (storage code <= 0x0E)
|
||||||
@@ -198,6 +261,14 @@ export async function detectDesfire(): Promise<DesfireDetectionResult> {
|
|||||||
// Larger storage indicates NTAG 424 DNA (~416 bytes)
|
// Larger storage indicates NTAG 424 DNA (~416 bytes)
|
||||||
chipType = ChipType.NTAG424_DNA;
|
chipType = ChipType.NTAG424_DNA;
|
||||||
}
|
}
|
||||||
|
} else if (hwProductType === PRODUCT_TYPES.NTAG && hwMajor >= 0x30) {
|
||||||
|
// NTAG 413 DNA returns product type 0x04 (like regular NTAG) but with:
|
||||||
|
// - hwMajor >= 0x30 (typically 0x30)
|
||||||
|
// - hwStorageSize 0x11 (different meaning than regular NTAG)
|
||||||
|
// - Responds to IsoDep (unlike regular NTAG 21x)
|
||||||
|
// Per NXP NT4H1321 datasheet, NTAG 413 DNA uses this response format
|
||||||
|
console.log('[DESFire] Detected NTAG 413 DNA (product type 0x04, hwMajor 0x30+)');
|
||||||
|
chipType = ChipType.NTAG413_DNA;
|
||||||
} else if (hwProductType === PRODUCT_TYPES.NTAG_I2C) {
|
} else if (hwProductType === PRODUCT_TYPES.NTAG_I2C) {
|
||||||
// NTAG I2C family (can have ISO-DEP on Plus variants)
|
// NTAG I2C family (can have ISO-DEP on Plus variants)
|
||||||
// Per NXP NT3H2111/NT3H2211 datasheet:
|
// Per NXP NT3H2111/NT3H2211 datasheet:
|
||||||
@@ -430,31 +501,58 @@ export async function detectSpark2Implant(): Promise<Spark2DetectionResult> {
|
|||||||
try {
|
try {
|
||||||
console.log('[DESFire] Checking for Spark 2 implant NDEF...');
|
console.log('[DESFire] Checking for Spark 2 implant NDEF...');
|
||||||
|
|
||||||
// Step 1: Select NDEF application
|
// Step 1: Select NDEF application (D2760000850101)
|
||||||
|
console.log('[DESFire] Selecting NDEF application...');
|
||||||
const selectNdefApp = await sendIsoDepCommand(selectAid(KNOWN_AIDS.ndefTag));
|
const selectNdefApp = await sendIsoDepCommand(selectAid(KNOWN_AIDS.ndefTag));
|
||||||
const selectAppResponse = parseApduResponse(selectNdefApp);
|
const selectAppResponse = parseApduResponse(selectNdefApp);
|
||||||
|
console.log('[DESFire] NDEF app select result:', {
|
||||||
|
sw: `${selectAppResponse.sw1.toString(16)}${selectAppResponse.sw2.toString(16)}`,
|
||||||
|
success: selectAppResponse.isSuccess,
|
||||||
|
});
|
||||||
|
|
||||||
if (!selectAppResponse.isSuccess) {
|
if (!selectAppResponse.isSuccess) {
|
||||||
console.log('[DESFire] NDEF app selection failed');
|
console.log('[DESFire] NDEF app selection failed');
|
||||||
return {found: false};
|
return {found: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Select NDEF file (file ID E104 for standard NDEF)
|
// Step 2: Select NDEF file (file ID E104 for standard Type 4 Tag)
|
||||||
// SELECT command: 00 A4 00 0C 02 E104
|
// SELECT by file ID: 00 A4 00 0C 02 E104
|
||||||
|
console.log('[DESFire] Selecting NDEF file E104...');
|
||||||
const selectFileCmd = [0x00, 0xa4, 0x00, 0x0c, 0x02, 0xe1, 0x04];
|
const selectFileCmd = [0x00, 0xa4, 0x00, 0x0c, 0x02, 0xe1, 0x04];
|
||||||
const selectFileResponse = await sendIsoDepCommand(selectFileCmd);
|
const selectFileResponse = await sendIsoDepCommand(selectFileCmd);
|
||||||
const selectFileParsed = parseApduResponse(selectFileResponse);
|
const selectFileParsed = parseApduResponse(selectFileResponse);
|
||||||
|
console.log('[DESFire] NDEF file select result:', {
|
||||||
|
sw: `${selectFileParsed.sw1.toString(16)}${selectFileParsed.sw2.toString(16)}`,
|
||||||
|
success: selectFileParsed.isSuccess,
|
||||||
|
});
|
||||||
|
|
||||||
if (!selectFileParsed.isSuccess) {
|
if (!selectFileParsed.isSuccess) {
|
||||||
console.log('[DESFire] NDEF file selection failed');
|
// Try alternate file ID (some Type 4 tags use E102 or just 02)
|
||||||
return {found: false};
|
console.log('[DESFire] Trying alternate NDEF file 0002...');
|
||||||
|
const altSelectCmd = [0x00, 0xa4, 0x00, 0x0c, 0x02, 0x00, 0x02];
|
||||||
|
const altSelectResponse = await sendIsoDepCommand(altSelectCmd);
|
||||||
|
const altSelectParsed = parseApduResponse(altSelectResponse);
|
||||||
|
console.log('[DESFire] Alternate file select result:', {
|
||||||
|
sw: `${altSelectParsed.sw1.toString(16)}${altSelectParsed.sw2.toString(16)}`,
|
||||||
|
success: altSelectParsed.isSuccess,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!altSelectParsed.isSuccess) {
|
||||||
|
console.log('[DESFire] NDEF file selection failed');
|
||||||
|
return {found: false};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Read NDEF length (first 2 bytes)
|
// Step 3: Read NDEF length (first 2 bytes)
|
||||||
// READ BINARY: 00 B0 00 00 02
|
// READ BINARY: 00 B0 00 00 02
|
||||||
|
console.log('[DESFire] Reading NDEF length...');
|
||||||
const readLengthCmd = [0x00, 0xb0, 0x00, 0x00, 0x02];
|
const readLengthCmd = [0x00, 0xb0, 0x00, 0x00, 0x02];
|
||||||
const readLengthResponse = await sendIsoDepCommand(readLengthCmd);
|
const readLengthResponse = await sendIsoDepCommand(readLengthCmd);
|
||||||
const lengthParsed = parseApduResponse(readLengthResponse);
|
const lengthParsed = parseApduResponse(readLengthResponse);
|
||||||
|
console.log('[DESFire] NDEF length read result:', {
|
||||||
|
sw: `${lengthParsed.sw1.toString(16)}${lengthParsed.sw2.toString(16)}`,
|
||||||
|
data: lengthParsed.data,
|
||||||
|
});
|
||||||
|
|
||||||
if (!lengthParsed.isSuccess || lengthParsed.data.length < 2) {
|
if (!lengthParsed.isSuccess || lengthParsed.data.length < 2) {
|
||||||
console.log('[DESFire] NDEF length read failed');
|
console.log('[DESFire] NDEF length read failed');
|
||||||
@@ -462,39 +560,58 @@ export async function detectSpark2Implant(): Promise<Spark2DetectionResult> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ndefLength = (lengthParsed.data[0] << 8) | lengthParsed.data[1];
|
const ndefLength = (lengthParsed.data[0] << 8) | lengthParsed.data[1];
|
||||||
console.log('[DESFire] NDEF length:', ndefLength);
|
console.log('[DESFire] NDEF message length:', ndefLength, 'bytes');
|
||||||
|
|
||||||
if (ndefLength === 0 || ndefLength > 500) {
|
if (ndefLength === 0 || ndefLength > 500) {
|
||||||
console.log('[DESFire] Invalid NDEF length');
|
console.log('[DESFire] Invalid or empty NDEF length');
|
||||||
return {found: false};
|
return {found: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Read NDEF message (starting after length bytes)
|
// Step 4: Read NDEF message (starting after length bytes)
|
||||||
// READ BINARY: 00 B0 00 02 <length>
|
// For longer NDEF messages, we may need to read in chunks
|
||||||
const readDataCmd = [0x00, 0xb0, 0x00, 0x02, Math.min(ndefLength, 128)];
|
const allNdefData: number[] = [];
|
||||||
const readDataResponse = await sendIsoDepCommand(readDataCmd);
|
let offset = 2; // Start after the 2-byte length field
|
||||||
const dataParsed = parseApduResponse(readDataResponse);
|
let remaining = ndefLength;
|
||||||
|
|
||||||
if (!dataParsed.isSuccess || dataParsed.data.length === 0) {
|
while (remaining > 0) {
|
||||||
console.log('[DESFire] NDEF data read failed');
|
const chunkSize = Math.min(remaining, 128); // Read up to 128 bytes at a time
|
||||||
|
// READ BINARY: 00 B0 <offset high> <offset low> <length>
|
||||||
|
const readDataCmd = [0x00, 0xb0, (offset >> 8) & 0xff, offset & 0xff, chunkSize];
|
||||||
|
const readDataResponse = await sendIsoDepCommand(readDataCmd);
|
||||||
|
const dataParsed = parseApduResponse(readDataResponse);
|
||||||
|
|
||||||
|
if (!dataParsed.isSuccess || dataParsed.data.length === 0) {
|
||||||
|
console.log('[DESFire] NDEF data read failed at offset', offset);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
allNdefData.push(...dataParsed.data);
|
||||||
|
offset += dataParsed.data.length;
|
||||||
|
remaining -= dataParsed.data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allNdefData.length === 0) {
|
||||||
|
console.log('[DESFire] No NDEF data read');
|
||||||
return {found: false};
|
return {found: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'[DESFire] NDEF data:',
|
'[DESFire] NDEF data (' + allNdefData.length + ' bytes):',
|
||||||
dataParsed.data.map(b => b.toString(16).padStart(2, '0')).join(' '),
|
allNdefData.slice(0, 50).map(b => b.toString(16).padStart(2, '0')).join(' ') +
|
||||||
|
(allNdefData.length > 50 ? '...' : ''),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert to ASCII and look for vivokey.co pattern
|
// Convert to ASCII and look for vivokey.co pattern
|
||||||
const asciiStr = dataParsed.data
|
const asciiStr = allNdefData
|
||||||
.filter(b => b >= 0x20 && b <= 0x7e)
|
.filter(b => b >= 0x20 && b <= 0x7e)
|
||||||
.map(b => String.fromCharCode(b))
|
.map(b => String.fromCharCode(b))
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
console.log('[DESFire] NDEF ASCII:', asciiStr);
|
console.log('[DESFire] NDEF ASCII:', asciiStr);
|
||||||
|
|
||||||
// Check for vivokey.co URL pattern
|
// Check for vivokey.co URL pattern - match any characters after the domain
|
||||||
const vivokeyMatch = asciiStr.match(/vivokey\.co\/([A-Za-z0-9]+)/i);
|
// Pattern allows alphanumeric, hyphens, underscores, and other URL-safe chars
|
||||||
|
const vivokeyMatch = asciiStr.match(/vivokey\.co\/([A-Za-z0-9_\-]+)/i);
|
||||||
if (vivokeyMatch) {
|
if (vivokeyMatch) {
|
||||||
const code = vivokeyMatch[1];
|
const code = vivokeyMatch[1];
|
||||||
const url = `https://vivokey.co/${code}`;
|
const url = `https://vivokey.co/${code}`;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
hasIsoDepCapability,
|
hasIsoDepCapability,
|
||||||
detectSakSwap,
|
detectSakSwap,
|
||||||
} from './mifare';
|
} from './mifare';
|
||||||
import {detectDesfire, detectDesfireFromAts, detectSpark2Implant} from './desfire';
|
import {detectDesfire, detectDesfireFromAts, detectSpark2Implant, detectSpark2FromNdef} from './desfire';
|
||||||
import {detectIso15693, isIso15693, detectSparkImplant} from './iso15693';
|
import {detectIso15693, isIso15693, detectSparkImplant} from './iso15693';
|
||||||
import {detectJavaCard, mightBeJavaCard, detectJavaCardFromAts} from './javacard';
|
import {detectJavaCard, mightBeJavaCard, detectJavaCardFromAts} from './javacard';
|
||||||
|
|
||||||
@@ -265,15 +265,24 @@ export async function detectChip(
|
|||||||
desfireResult.chipType === ChipType.NTAG413_DNA;
|
desfireResult.chipType === ChipType.NTAG413_DNA;
|
||||||
|
|
||||||
if (isNtagDna) {
|
if (isNtagDna) {
|
||||||
try {
|
// First, try to detect from cached NDEF records (doesn't require APDU)
|
||||||
onProgress?.('Reading NDEF for Spark 2...');
|
// This works even after DESFire GET_VERSION puts the tag in native mode
|
||||||
const spark2Result = await detectSpark2Implant();
|
const cachedNdefResult = detectSpark2FromNdef(rawData.ndefRecords);
|
||||||
if (spark2Result.found && spark2Result.name) {
|
if (cachedNdefResult.found && cachedNdefResult.name) {
|
||||||
implantName = spark2Result.name;
|
implantName = cachedNdefResult.name;
|
||||||
console.log('[Detector] Found Spark 2 implant:', implantName);
|
console.log('[Detector] Found Spark 2 implant from cached NDEF:', implantName);
|
||||||
|
} else {
|
||||||
|
// Fallback: Try APDU-based NDEF reading (may fail if tag is in native mode)
|
||||||
|
try {
|
||||||
|
onProgress?.('Reading NDEF for Spark 2...');
|
||||||
|
const spark2Result = await detectSpark2Implant();
|
||||||
|
if (spark2Result.found && spark2Result.name) {
|
||||||
|
implantName = spark2Result.name;
|
||||||
|
console.log('[Detector] Found Spark 2 implant via APDU:', implantName);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Detector] Spark 2 APDU detection failed:', e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.warn('[Detector] Spark 2 detection failed:', e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -819,19 +819,20 @@ export async function detectSparkImplant(
|
|||||||
console.log('[ISO15693] Found Spark URL:', url);
|
console.log('[ISO15693] Found Spark URL:', url);
|
||||||
|
|
||||||
// Determine Spark version based on chip type
|
// Determine Spark version based on chip type
|
||||||
// SLIX = Spark 1, NTAG 424 DNA = Spark 2
|
// SLIX/ICODE DNA = Spark 1 (ISO 15693)
|
||||||
|
// NTAG 424 DNA = Spark 2 (ISO 14443-4, handled in desfire.ts)
|
||||||
let sparkName: string;
|
let sparkName: string;
|
||||||
if (
|
if (
|
||||||
chipType === ChipType.SLIX ||
|
chipType === ChipType.SLIX ||
|
||||||
chipType === ChipType.SLIX_S ||
|
chipType === ChipType.SLIX_S ||
|
||||||
chipType === ChipType.SLIX_L ||
|
chipType === ChipType.SLIX_L ||
|
||||||
chipType === ChipType.SLIX2
|
chipType === ChipType.SLIX2 ||
|
||||||
|
chipType === ChipType.ICODE_DNA
|
||||||
) {
|
) {
|
||||||
sparkName = 'Spark 1';
|
sparkName = 'Spark 1';
|
||||||
} else if (chipType === ChipType.NTAG424_DNA) {
|
|
||||||
sparkName = 'Spark 2';
|
|
||||||
} else {
|
} else {
|
||||||
sparkName = 'Spark';
|
// Any other ISO 15693 chip with vivokey.co URL is likely a Spark variant
|
||||||
|
sparkName = 'Spark 1';
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
* Matches detected NFC chips to compatible Dangerous Things products
|
* Matches detected NFC chips to compatible Dangerous Things products
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ChipType, getChipFamily, CHIP_CLONEABILITY, ChipFamily} from '../../types/detection';
|
import { ChipType, getChipFamily, CHIP_CLONEABILITY, ChipFamily, Transponder } from '../../types/detection';
|
||||||
import {MatchResult, Product, DesfireEvLevel} from '../../types/products';
|
import { MatchResult, Product, DesfireEvLevel } from '../../types/products';
|
||||||
import {
|
import {
|
||||||
PRODUCTS,
|
PRODUCTS,
|
||||||
getChipProductMap,
|
getChipProductMap,
|
||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
/**
|
/**
|
||||||
* Match a detected chip to compatible products
|
* Match a detected chip to compatible products
|
||||||
*/
|
*/
|
||||||
export function matchChipToProducts(chipType: ChipType): MatchResult {
|
export function matchChipToProducts(chip: Transponder): MatchResult {
|
||||||
|
const chipType = chip.type as ChipType
|
||||||
const chipProductMap = getChipProductMap();
|
const chipProductMap = getChipProductMap();
|
||||||
const cloneability = CHIP_CLONEABILITY[chipType];
|
const cloneability = CHIP_CLONEABILITY[chipType];
|
||||||
const chipFamily = getChipFamily(chipType);
|
const chipFamily = getChipFamily(chipType);
|
||||||
@@ -32,7 +33,12 @@ export function matchChipToProducts(chipType: ChipType): MatchResult {
|
|||||||
exactMatches.push(product);
|
exactMatches.push(product);
|
||||||
}
|
}
|
||||||
if (product.canReceiveClone && cloneability?.cloneable) {
|
if (product.canReceiveClone && cloneability?.cloneable) {
|
||||||
cloneTargets.push(product);
|
if ((product.name.startsWith("xMagic") || product.name.startsWith("xM1") || product.name.startsWith("flexM1")) && chip.rawData.uid.replaceAll(":", "").length / 2 !== 4) {
|
||||||
|
// This skips things without a 4-byte UID
|
||||||
|
|
||||||
|
} else {
|
||||||
|
cloneTargets.push(product);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
ScanError,
|
ScanError,
|
||||||
ScanErrorType,
|
ScanErrorType,
|
||||||
NfcTechType,
|
NfcTechType,
|
||||||
|
NdefRecord,
|
||||||
} from '../../types/nfc';
|
} from '../../types/nfc';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,6 +116,47 @@ function parseAts(tag: TagEvent): {ats?: string; historicalBytes?: string} {
|
|||||||
return {ats, historicalBytes};
|
return {ats, historicalBytes};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse NDEF records from TagEvent
|
||||||
|
*/
|
||||||
|
function parseNdefRecords(tag: TagEvent): NdefRecord[] | undefined {
|
||||||
|
const ndefMessage = (tag as any).ndefMessage;
|
||||||
|
if (!ndefMessage || !Array.isArray(ndefMessage) || ndefMessage.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const records: NdefRecord[] = [];
|
||||||
|
for (const record of ndefMessage) {
|
||||||
|
if (!record) continue;
|
||||||
|
|
||||||
|
// react-native-nfc-manager returns NDEF records with these properties
|
||||||
|
const tnf = record.tnf ?? 0;
|
||||||
|
const type = record.type
|
||||||
|
? typeof record.type === 'string'
|
||||||
|
? record.type
|
||||||
|
: String.fromCharCode(...(Array.isArray(record.type) ? record.type : []))
|
||||||
|
: '';
|
||||||
|
const id = record.id
|
||||||
|
? typeof record.id === 'string'
|
||||||
|
? record.id
|
||||||
|
: String.fromCharCode(...(Array.isArray(record.id) ? record.id : []))
|
||||||
|
: undefined;
|
||||||
|
const payload = Array.isArray(record.payload)
|
||||||
|
? record.payload
|
||||||
|
: typeof record.payload === 'string'
|
||||||
|
? record.payload.split('').map((c: string) => c.charCodeAt(0))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
records.push({tnf, type, id, payload});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (records.length > 0) {
|
||||||
|
console.log('[NFCManager] Parsed NDEF records:', records.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return records.length > 0 ? records : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert TagEvent to RawTagData
|
* Convert TagEvent to RawTagData
|
||||||
*/
|
*/
|
||||||
@@ -124,6 +166,7 @@ function tagEventToRawData(tag: TagEvent): RawTagData {
|
|||||||
const sak = parseSak(tag);
|
const sak = parseSak(tag);
|
||||||
const atqa = parseAtqa(tag);
|
const atqa = parseAtqa(tag);
|
||||||
const {ats, historicalBytes} = parseAts(tag);
|
const {ats, historicalBytes} = parseAts(tag);
|
||||||
|
const ndefRecords = parseNdefRecords(tag);
|
||||||
|
|
||||||
const isoDep = (tag as any).isoDep;
|
const isoDep = (tag as any).isoDep;
|
||||||
const iso7816 = (tag as any).iso7816;
|
const iso7816 = (tag as any).iso7816;
|
||||||
@@ -160,6 +203,7 @@ function tagEventToRawData(tag: TagEvent): RawTagData {
|
|||||||
ats,
|
ats,
|
||||||
historicalBytes,
|
historicalBytes,
|
||||||
maxTransceiveLength,
|
maxTransceiveLength,
|
||||||
|
ndefRecords,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,20 @@ export type NfcTechType =
|
|||||||
| 'Iso7816'
|
| 'Iso7816'
|
||||||
| 'Iso15693';
|
| 'Iso15693';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NDEF record from tag
|
||||||
|
*/
|
||||||
|
export interface NdefRecord {
|
||||||
|
/** Type Name Format */
|
||||||
|
tnf: number;
|
||||||
|
/** Record type as string */
|
||||||
|
type: string;
|
||||||
|
/** Record ID */
|
||||||
|
id?: string;
|
||||||
|
/** Payload data as byte array */
|
||||||
|
payload: number[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Raw tag data from NFC scan
|
* Raw tag data from NFC scan
|
||||||
*/
|
*/
|
||||||
@@ -36,6 +50,8 @@ export interface RawTagData {
|
|||||||
historicalBytes?: string;
|
historicalBytes?: string;
|
||||||
/** Maximum transceive length */
|
/** Maximum transceive length */
|
||||||
maxTransceiveLength?: number;
|
maxTransceiveLength?: number;
|
||||||
|
/** Cached NDEF records (if tag supports NDEF) */
|
||||||
|
ndefRecords?: NdefRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user