NFC Host-based card emulation (HCE)

Yes, I saw, and thx for the awesome work! :slight_smile:

@wodin do you have any idea, what’s could be the problem?

Please check these:

  • I think it’s good
My app.json
"plugins": [
            [
                "./plugin/withReactNativeHce.js",
                {
                    "appIds": [
                        "A0000001020304"
                    ]
                }
            ],
            [
                "@config-plugins/react-native-ble-plx",
                {
                    "isBackgroundEnabled": true,
                    "modes": [
                        "peripheral",
                        "central"
                    ],
                    "bluetoothAlwaysPermission": "Allow $(PRODUCT_NAME) to connect to bluetooth devices",
                    "bluetoothPeripheralPermission": "Allow $(PRODUCT_NAME) to connect to bluetooth devices"
                }
            ],
            [
                "react-native-nfc-manager",
                {
                    "nfcPermission": "It's for Senity Security Systems Ltd.",
                    "selectIdentifiers": [
                        "A0000002471001",
                        "D2760000850100",
                        "D2760000850101",
                        "A0000003964D66344D0002"
                    ]
                }
            ]
        ]
  • I think this could be bad
My Activity where I start the simulation
import LottieView from 'lottie-react-native';
import React, { useState, useEffect  } from 'react';
import {Platform, StyleSheet, Text, View,
        Image, TouchableOpacity, Alert, Vibration,
        BackHandler, AppState, Modal, Button
        } from 'react-native';
import 'react-native-gesture-handler';
import { Colors } from 'react-native/Libraries/NewAppScreen';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import * as ScreenOrientation from 'expo-screen-orientation';
import AppLoading from 'expo-app-loading';
import DeviceInfo from 'react-native-device-info';
import RNExitApp from 'react-native-exit-app';
import {widthPercentageToDP as wp, heightPercentageToDP as hp} from 'react-native-responsive-screen';
import * as Font from 'expo-font';
import NfcManager, {NfcTech, Ndef, NfcEvents, NfcAdapter, NfcError} from 'react-native-nfc-manager';
import * as SecureStore from 'expo-secure-store';
import HCESession, { NFCContentType, NFCTagType4 } from 'react-native-hce';
let simulation;

let uniqueId2 = DeviceInfo.getUniqueId();
let uniqueId = uniqueId2.slice(0,13);
let NFCData = false;
let readNDEF_bool = false;
let got_uid_2;
let read_or_write = false;
let ONE_SECOND_IN_MS = 100;
let SendBool = false



async function initNfc() {
  await NfcManager.start();
  console.log("NFC Manager has just started...");
}
export async function save(key, value) {
  await SecureStore.setItemAsync(key, value);
}
export async function getValueFor(key) {
  let result = await SecureStore.getItemAsync(key);
  if (result) {
    alert("🔐 Here's your value 🔐 \n" + result);
  } else {
    alert('No values stored under that key.');
  }
}
//HACK: It's for debugging purposes only
async function deleteFor(key) {
  SecureStore.deleteItemAsync(key);
  getValueFor(key);
}

let writeNdef = async ({ type, value}) => {
  console.log("itt vagyok")
  SendBool = true
  let result = false;
  try {
    console.log("||||||||||||||||||||||||||||||||||||||||");
    
    console.log("*****************************************");
    //const bytes = Ndef.encodeMessage([Ndef.textRecord("1B226FB2-F006-4BC2-A613-7C69ECB")]);
    //const bytes = Ndef.encodeMessage([Ndef.textRecord("1B226FB2-F006-4BC2-A613-7C69ECB0")]);
    let bytes;
    if(!read_or_write)
    {
      await NfcManager.requestTechnology(NfcTech.Ndef, {
        alertMessage: 'Ready to Send Your Unique ID to Senity',
      });
      //this.setState({send: true})
      
      console.log("write unique ID");
      console.log(uniqueId);
      bytes = Ndef.encodeMessage([Ndef.textRecord(uniqueId)]);
    }else if(read_or_write)
    {
      //this.setState({NFCData_bool: false})
      await NfcManager.requestTechnology(NfcTech.Ndef, {
        alertMessage: 'Ready to Send Your generated UID to Senity',
      });
      console.log("write generated ID");
      console.log(got_uid_2);
      bytes = Ndef.encodeMessage([Ndef.textRecord(got_uid_2)]);
    }      
    if (bytes) {
      await NfcManager.ndefHandler // Step2
        .writeNdefMessage(bytes); // Step3

      if (Platform.OS === 'ios') {
        await NfcManager.setAlertMessageIOS('Successfully sent NDEF');


        NFCData = true;
        Vibration.vibrate();
      }else
      {
        await NfcManager.setAlertMessage('Successfully sent NDEF')
        NFCData = true;
        Vibration.vibrate(50, 500);
      }
      /*this.setState({approved: true})
      this.setState({pushed: false})
      this.setState({error: false})*/
      if(read_or_write)
      {
        setTimeout(() =>{
          RNExitApp.exitApp();
        }, 2500);
      }
    }
    result = true;
  } catch (ex) {
    //console.warn(ex);
    if (ex instanceof NfcError.UserCancel) {
      // bypass
      console.log("User Canceled");
      //NFCData = false;
      Vibration.vibrate();
      let UCanceled = "User Canceled"
      return UCanceled;
    } else if (ex instanceof NfcError.Timeout) {
      Alert.alert('NFC Session Timeout');
      NFCData = false;
    }else{
      console.warn(ex);
      NFCData = false;
      if (Platform.OS === 'ios') {
        NfcManager.invalidateSessionWithErrorIOS(`${ex}`);
      } else {
        //Alert.alert('NFC Error', `${ex}`);
      }
    }
  }
  finally
  {
    NfcManager.cancelTechnologyRequest();
    console.log("Finally");
    
    console.log(NFCData);
    
  }
  
  // Step 4
  NfcManager.cancelTechnologyRequest().catch(() => 0);
  return NFCData;
}
let _cleanUp = () => {
    NfcManager.cancelTechnologyRequest().catch(() => 0);
  }
function readNdef() {
  console.log("readNdef");
  
    const cleanUp = () => {
      NfcManager.setEventListener(NfcEvents.DiscoverTag, null);
      NfcManager.setEventListener(NfcEvents.SessionClosed, null);
    };
    return new Promise((resolve) => {
      let tagFound = null;
      let parsed = null;
      NfcManager.setEventListener(NfcEvents.DiscoverTag, (tag) => {
        tagFound = tag;
        resolve(tagFound);

        console.log(tagFound);
        record = tagFound;
        const ndefRecords = tag.ndefMessage;
        function decodeNdefRecord(record)
        {
          if(Ndef.isType(record, Ndef.TNF_WELL_KNOWN, Ndef.RTD_TEXT)){
            let NdefCode = Ndef.text.decodePayload(record.payload);
            console.log("Data From PN532 is: ", NdefCode);
            //console.log(['text', Ndef.text.decodePayload(record.payload)]);
            //TODO:Here we have to save it into asyncStorage
            save('genuid', NdefCode);
            readNDEF_bool = true;
            NFCData = false;
            return ['text', Ndef.text.decodePayload(record.payload)];
          }else if(Ndef.isType(record, Ndef.TNF_WELL_KNOWN, Ndef.RTD_URI))
          {
            return ['uri', Ndef.uri.decodePayload(record.payload)];
          }
          return ['unknown', '---']
        }
        parsed = ndefRecords.map(decodeNdefRecord);
        if(Platform.OS === "ios")
        {
          NfcManager.setAlertMessageIOS('NDEF tag found');
        }else
        {
          NfcManager.setAlertMessage('NDEF tag found');
        }

        NfcManager.unregisterTagEvent().catch(() => 0);
      });

      NfcManager.setEventListener(NfcEvents.SessionClosed, () => {
        cleanUp();
        if (!tagFound) {
          resolve();
        }
      });

      NfcManager.registerTagEvent();
    });
}
export default class IosGetuidScreen extends React.Component {

  constructor(props) {
        super(props);
        this.state={
            fontLoaded:false,
            UID:'',
            DeviceID: '',
            getUID: '',
            NFCSTATE: '',
            text: 'Tap to the virtual card to register your device',
            key: 'genuid',
            uidshow: false,
            uidtext: '',
            onlywrite: false,
            Vibrate_pattern: [1 * ONE_SECOND_IN_MS],
            restart_var: false,
            send: false,
            receive: false,
            error: false,
            canceled: false,
            approved: false,
            device_platform:true,
            NFCData_bool: true,
            pushed: false,
            NFCStatus: 'Sending your ID!',
            NFCStatusColor: '#212832'

            
            
        }
        
    }
  //this.setState({DeviceID:uniqueId});
_cleanUp = () => {
    NfcManager.cancelTechnologyRequest().catch(() => 0);
  };
_testNdef = async () => {
    //readNdef();
    if(NFCData && !read_or_write)
    {
      this.setState({ NFCData_bool: false })
      await readNdef();
            
      if (readNDEF_bool)
      {
        if(Platform.OS === "ios")
        {
          Vibration.vibrate();
        }else
        {
          Vibration.vibrate(50, 500);
        }
        this.setState({NFCStatus: 'Success!'})
        this.setState({approved: true})
        setTimeout(() =>{
          this.setState({NFCData_bool: true})
        }, 2500);
        this.setState({ text: '' });
        this.setState({ uidshow: true})
        let got_uid = await SecureStore.getItemAsync('genuid');
        console.log("got_uid");
        console.log(got_uid);
        let rm_aster_uid = got_uid.substring(0, got_uid.length - 2);
        this.setState({ uidtext: rm_aster_uid})
        read_or_write = true;
        
        this.setState({ text: 'Tap to authorize' });
        //this.setState({NFCStatus: 'Authorization!'})
      }else if(NFCData && !readNDEF_bool)
      {
        this.setState({ NFCData_bool: false })
        
        setTimeout(() =>{
          this.setState({NFCData_bool: true})
        }, 2500);
      }else if(!readNDEF_bool || rm_aster_uid.length === 0)
      {
        Vibration.vibrate();
        this.setState({restart_var: true})
        this.setState({ text: 'An error occured.\nPlease restart the process!'});

        
      }
    } else if (!NFCData)
    {
      
      readNDEF_bool ? this.setState({ uidshow: true }) : this.setState({ uidshow: false }); 
      if(!read_or_write)
      {
        this.setState({ text: 'Tap to the virtual card to register your device' });
        this.setState({ NFCData_bool: false })
      }else if(read_or_write)
      {
        this.setState({ NFCData_bool: false })
      }
      let WriteResult = await writeNdef({});
      console.log("WriteResult: " + WriteResult)
      if((typeof(WriteResult) === "boolean") && WriteResult)
      {
        console.log("Hellokaa1")
        this.setState({approved: true})
        this.setState({NFCStatusColor: 'blue'})
        this.setState({error: false})
        this.setState({NFCStatus: 'Success!'})
        setTimeout(() =>{
          this.setState({NFCData_bool: true})
        }, 1800);
        
      }else if((typeof(WriteResult) != "boolean") && WriteResult.startsWith("User Canceled"))
      {
        console.log("Hellokaa2")
        this.setState({error: false})
        this.setState({approved: false})
        this.setState({pushed: true})
        this.setState({NFCStatus: 'Sending your ID!'})
      }
      else
      {
        console.log("Hellokaa3")
        this.setState({error: true})
        this.setState({approved: false})
        this.setState({NFCStatus: 'Error!'})
        this.setState({NFCStatusColor: 'red'})
      }
    
      if (NFCData && !readNDEF_bool)
      {
        this.setState({ text: 'Tap again to get the generated UID' });
        
      }
    }
  };
  // Pre-step, call this before any NFC operations
  
  async componentDidMount(){
    if(Platform.OS === "ios")
      {
        this.setState({device_platform: false})
      }
    BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
    this.setState({fontLoaded:true});
    console.log("ID: ");
    console.log(uniqueId2);
    this.setState({DeviceID:uniqueId2});
    //HACK: it's for debugging purposes only
    //deleteFor('genuid');
    got_uid_2 = await SecureStore.getItemAsync('genuid');
    console.log(got_uid_2);
    if (got_uid_2)
    {
      let sliced_got_uid_2 = got_uid_2.substring(0, got_uid_2.length - 2)
      this.setState({ uidtext: sliced_got_uid_2 })
      this.setState({ uidshow: true })
      this.setState({ onlywrite: true})
      read_or_write = true;
      readNDEF_bool = true;
      this.setState({text: ''})
      this.setState({ text: 'Tap to authorize' });
      this._testNdef();
    }
    else
    {
      read_or_write = false;
      console.log("You do not have UID set yet");
      this.setState({ text: 'Tap to the virtual card to register your device'})
    }
  }
  
  async componentWillUnmount(){
    BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
    _cleanUp();
  }
  handleOnPress = () =>
  {
    this.setState({restart_var : false })
    this.setState({ text: 'Tap to the virtual card to register your device'})
    NFCData = false
    readNDEF_bool = false
    read_or_write = false

  }
  handleBackPress = () => {
    RNExitApp.exitApp();
  }
  cancelNFCAndroid = () =>{
    NfcManager.cancelTechnologyRequest();
    this.setState({NFCData_bool: true}) 
  }
  loadNFCAndroid = () =>{
    if(!(this.state.error || this.state.approved))
    {
      this.setState({pushed: true})
      this.setState({error: false})
      this.setState({approved: false})
    }else if(this.state.error || this.state.approved && read_or_write)
    {
      this.setState({pushed: true})
      this.setState({error: false})
      this.setState({approved: false})
      this.setState({NFCStatus: 'Sending your ID!'})
    }else if(this.state.error || this.state.approved && (!readNDEF_bool && NFCData))
    {
      this.setState({pushed: true})
      this.setState({error: false})
      this.setState({approved: false})
      this.setState({NFCStatus: 'Getting your UID!'})
    }else
    {
      this.setState({pushed: false})
      this.setState({error: false})
      this.setState({approved: false})
    }    
  }
  NCFstatus = () =>{
    if(this.state.approved)
    {
      return { color: 'green'}
    }else if(this.state.error)
    {
      return { color: 'red'}
    }
  }
  startSimulation = async () => {
    console.log("Simulation started!")
    const tag = new NFCTagType4(NFCContentType.Text, "Hello world");
    simulation = await (new HCESession(tag)).start();
  }
  
  stopSimulation = async () => {
    await simulation.terminate();
  }
  render() {
    return (
      
      <View style={background.main}>
        <Modal
          transparent={true}
          visible={this.state.device_platform && !this.state.NFCData_bool}
        >
          <View
            style={background.modal_background}
          >
            <View
              style={background.modal_load}
            >
              <View
                style={this.state.pushed && !(this.state.approved || this.state.error) ? background.EmptyView : background.text1hide}
              >
                <LottieView
                  ref={animation => {
                  this.animation = animation;
                  }}
                  style={background.lottie_load}
                  resizeMode="cover"
                  source={require('../../assets/loading_circle_v4.json')}
                  autoPlay
                >
                </LottieView>
              </View>
              <View
                style={this.state.error ? background.EmptyView : background.text1hide}
              >
                <LottieView
                  ref={animation => {
                  this.animation = animation;
                  }}
                  style={background.lottie_load}
                  resizeMode="cover"
                  source={require('../../assets/error_lottie.json')}
                  autoPlay
                >
                </LottieView>
              </View>
              <View
                style={this.state.approved ? background.EmptyView : background.text1hide}
              >
                <LottieView
                  ref={animation => {
                  this.animation = animation;
                  }}
                  style={background.lottie_load_approved}
                  resizeMode="cover"
                  source={require('../../assets/approved_lottie_v3.json')}
                  autoPlay
                >
                </LottieView>
              </View>
              <Text
                style={[background.modal_text, this.NCFstatus()]}
              >
                {this.state.NFCStatus}
              </Text>
              <TouchableOpacity
                style={background.button_cancel_tchopa}
                onPress={this.cancelNFCAndroid}
              >
                <Text
                  style={background.button_cancel_text}
                >
                  Cancel
                </Text>
              </TouchableOpacity>

            </View>
          </View>
        </Modal>
        <Text
            style={!this.state.restart_var ? background.text1show: background.restart_text}
        >
          {this.state.text}
        </Text>

        <View>
          <TouchableOpacity
            style={!this.state.restart_var ? background.Topacity1: background.display_none}
            onPress={()=>{
              this.startSimulation()
              //this.loadNFCAndroid()
              //this._testNdef();
            }} 
          >
          
            
          
            
            <Image
                style={!this.state.restart_var ? background.card: background.display_none}
              source={require('../../assets/card_blank.png')}
            >
            </Image>
            <Text style={this.state.uidshow ?
              background.textuidshow : background.text1hide}>
              {this.state.uidtext}
            </Text>
            {/*<Text style={
              background.textuidshow}>
              A4 B4 FF AA
            </Text>*/}
          </TouchableOpacity>
          <TouchableOpacity
            style={this.state.restart_var ?
              background.restart_touchableopacityshow: background.display_none}
              onPress={this.handleOnPress}
              >
            <LottieView
              ref={animation => {
                this.animation = animation;
              }}
              source={require('../../assets/restart.json')}
              autoPlay
            />
            
          </TouchableOpacity>
          <Text style={this.state.restart_var ? background.restart_label: background.display_none}>
            Restart
          </Text>
        </View>          
        <Text style={background.copyrighttext}> © 2021 Senity Security Systems Ltd.
        </Text>
      </View>
    );
     
  }  
}

const background = StyleSheet.create({
    main:{
        backgroundColor: '#212832',
        flex: 1,
        flexDirection: 'column',   
    },
    card:{
        //marginTop: '15%',
        //justifyContent: 'center',
        alignSelf: 'center',
        height: '100%',
        width: '100%',
        //resizeMode: 'stretch'
        //resizeMode: 'cover', 
        resizeMode: 'contain'
    },
    Topacity1:{
      //flex: 1,
      width: '95%',
      height: '55%',
      alignSelf: 'center',
    },
    textuidshow: {
      position: 'absolute',
      alignSelf: 'center',
      textAlign: 'center',
      marginTop: '38%',
      color: '#fff5ee',
      fontSize: 50,
      fontFamily: 'Apple SD Gothic Neo',
      fontWeight: 'bold',
      letterSpacing: 5,
      textTransform: 'uppercase'
    },
    text1show:{
        alignSelf: 'center',
        textAlign: 'center',
        marginTop: '20%',
        color: 'white',
        fontSize: 28,
        fontFamily: 'Apple SD Gothic Neo',
        fontWeight: 'bold',
    },
    text1hide:{
      display:'none'
    },
    copyrighttext: {
      bottom: '5%',
      position: 'absolute',
      alignSelf: 'center',
      color: 'white',
      fontFamily: 'Apple SD Gothic Neo',
      fontSize: 8,
    },
    restart_touchableopacityshow:
    {
        marginTop: '30%',
        alignSelf: 'center',
        height: 200,
        width: 200,
        //flex: 2
    },
    restart_lottie:
    {
      justifyContent: 'center',
      //marginTop: '5%',
      alignSelf: 'center',
      height: 5,
      width: 30
      //flex: 2
    },
    restart_text:
    {
      alignSelf: 'center',
      textAlign: 'center',
      marginTop: '20%',
      color: 'red',
      fontSize: 28,
      fontFamily: 'Apple SD Gothic Neo',
      fontWeight: 'bold',
    },
    restart_label:
    {
      position: 'absolute',
      bottom: 15,
      alignSelf: 'center',
      textAlign: 'center',
      color: 'red',
      fontSize: 20,
      fontFamily: 'Apple SD Gothic Neo',
    },
    display_none:
    {
      display: 'none'
    },
    modal_background:{
      flex: 1,
      backgroundColor: 'rgba(1, 0, 0, 0.5)',
      alignItems: 'center',
      justifyContent: 'center'
    },
    modal_load:{
      width: 280,
      height: 280,
      margin: 20,
      backgroundColor: "#fff5ee",
      borderRadius: 20,
      padding: 35,
      shadowColor: "black",
      shadowOffset: {
        width: 10,
        height: 2
    },
    },
    no_modal:{
      display: 'none'
    },
    lottie_load:{
      width: 125,
      height: 125,
      alignSelf: 'center'

    },
    lottie_load_approved:{
      width: wp(15),
      height: hp(15),
      alignSelf: 'center'

    },
    modal_text:{
      alignSelf: 'center',
      textAlign: 'center',
      color: '#212832',
      fontSize: 18,
      fontFamily: 'Apple SD Gothic Neo',
      paddingBottom: 25,
      fontWeight: 'bold'
    },
    button_cancel_tchopa:{
      width: 180,
      alignSelf: 'center',
      textAlign: 'center',
      borderRadius: 15,
      borderWidth: 1,
      backgroundColor: '#fff5ee'
    },
    button_cancel_text:{
      letterSpacing: 2.5,
      alignSelf: 'center',
      textAlign: 'center',
      color: '#212832',
      fontSize: 25,
      fontFamily: 'Apple SD Gothic Neo',
    },
    EmptyView:{

    }
});

So there are two functions:

startSimulation
startSimulation = async () => {
    console.log("Simulation started!")
    const tag = new NFCTagType4(NFCContentType.Text, "Hello world");
    simulation = await (new HCESession(tag)).start();
}
stopSimulation
stopSimulation = async () => {
    await simulation.terminate();
}

I call startSimulation at

onPress={()=>{
              this.startSimulation()
              //this.loadNFCAndroid()
              //this._testNdef();
}}

Finally, I get the 90 00 APDU code which means: Command successfully executed (OK).
but I do not get the message “Hello World”
Here is the list of APDU messages: APDU messages

I think that the Expo side of things is probably OK.

It would probably be best to ask someone who knows something about NFC and HCE.

It might also be worth trying to get the simplest possible plain React Native app working. If that doesn’t work, talk to the React Native HCE people or maybe Stack Overflow.

When the simple plain RN version works, try getting it to work in Expo with the config plugin. If it works as a plain RN app, but not in Expo with the config plugin, then try running expo prebuild and compare the results with the working RN app. Based on that we can figure out what changes might be needed in the config plugin.

Good luck.