NFC Host-based card emulation (HCE)

Hi there!
I know that hce only available for Android at present.
With eas build we can use native libaries too.
I found a hce solution react native host-based card emulation

My question is how to add aid_list.xml into expo, because hce needs it.
Thanks in advance!

Hi @freeridre

You should be able to create to file somewhere else in your project and copy it to the right location during the build process using the eas-build-post-install hook.

EAS Build-specific npm hooks

1 Like

Well, I still do not understand how to copy the file to the right location if I use managed workflow.
Here is my package.json

{

  "main": "node_modules/expo/AppEntry.js",

  "scripts": {

    "start": "expo start",

    "android": "expo start --android",

    "ios": "expo start --ios",

    "web": "expo start --web",

    "eject": "expo eject",

    "eas-build-pre-install": "echo 123",

    "eas-build-post-install": "echo 456",

    "eas-build-pre-upload-artifacts": "echo 789"

  },

  "dependencies": {

    "@config-plugins/react-native-ble-plx": "^0.0.1",

    "@expo/metro-config": "^0.2.2",

    "@react-native-community/masked-view": "0.1.10",

    "@react-navigation/native": "^5.9.4",

    "@react-navigation/stack": "^5.14.5",

    "expo": "~42.0.1",

    "expo-app-loading": "^1.1.2",

    "expo-dev-client": "^0.4.5",

    "expo-device": "~3.3.0",

    "expo-font": "~9.2.1",

    "expo-local-authentication": "~11.1.1",

    "expo-screen-orientation": "~3.2.1",

    "expo-secure-store": "~10.2.0",

    "expo-status-bar": "~1.0.4",

    "expo-updates": "^0.8.5",

    "lottie-react-native": "4.0.2",

    "react": "16.13.1",

    "react-dom": "16.13.1",

    "react-native": "https://github.com/expo/react-native/archive/sdk-42.0.0.tar.gz",

    "react-native-ble-plx": "^2.0.2",

    "react-native-device-info": "^8.1.4",

    "react-native-exit-app": "^1.1.0",

    "react-native-gesture-handler": "~1.10.2",

    "react-native-nfc-manager": "^3.11.0",

    "react-native-password-eye": "^1.0.2",

    "react-native-password-strength-meter": "0.0.5",

    "react-native-reanimated": "~2.2.0",

    "react-native-safe-area-context": "3.2.0",

    "react-native-screens": "~3.4.0",

    "react-native-vector-icons": "^8.1.0",

    "react-native-web": "^0.17.1"

  },

  "devDependencies": {

    "@babel/core": "^7.9.0"

  },

  "private": true

}

@wodin Are there any tutorial, hogy to copy it like in the “eas-build-post-install”: “echo 456” ?

Assuming you create your aid_list.xml file in a directory called res and you add it to Git, then I think the following would work:

  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "eject": "expo eject",
    "eas-build-post-install": "mkdir -p android/app/src/main/res/xml && cp -v res/aid_list.xml android/app/src/main/res/xml",
  },
1 Like

Well, thank you, I will try it. And I have one more question.
How to add these permission into androidmanifest.xml in managed expo project?

<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/service_name"
    android:requireDeviceUnlock="false">
    <aid-group
        android:category="other"
        android:description="@string/card_title">
        <aid-filter android:name="F201808175" />
    </aid-group>
</host-apdu-service>

Ah. For that you’d need a config plugin. And if you’re going to write a config plugin you might as well generate the aid_list.xml file in that too.

See Config Plugins - Expo Documentation
I believe you want to use withAndroidManifest.
Clone the https://github.com/expo/expo/ repository and run this to find examples:

git grep -l withAndroidManifest -- packages/*/plugin/src/

Here are some posts on the forums that have examples too (in no particular order):

Also maybe have a look at xml2js for generating aid_list.xml.

EDIT: Here’s another example:

Well, I try to write my own config plugin for android host-based card emulation, but it’s a little bit overkill for me. Can you help me to write the config plugin? :confused:

Remove the post install hook and try this.

I have only tested that it makes the changes specified here. I have not actually tried to use it in an app.

Create a file called plugin/withReactNativeHce.js with the following contents:

const { withAndroidManifest, withPlugins } = require("@expo/config-plugins");
const xml2js = require("xml2js");
const { mkdirSync, writeFileSync } = require("fs");

const NfcHceServiceXml = `
<service
  android:name="com.reactnativehce.services.CardService"
  android:exported="true"
  android:enabled="false"
  android:permission="android.permission.BIND_NFC_SERVICE">
  <intent-filter>
    <action
      android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
    <category android:name="android.intent.category.DEFAULT"/>
  </intent-filter>
  <meta-data
    android:name="android.nfc.cardemulation.host_apdu_service"
    android:resource="@xml/aid_list"/>
</service>`;

let NfcHceService;
xml2js.parseString(NfcHceServiceXml, (err, result) => (NfcHceService = result.service));

function withNfcHceAndroidManifest(config, { appIds }) {
  return withAndroidManifest(config, (config) => {
    config.modResults = addNfcPermissionToManifest(config.modResults);
    config.modResults = addNfcHceHardwareFeatureToManifest(config.modResults);
    config.modResults = addNfcHceServiceToManifest(config.modResults);
    writeAidList(appIds);
    return config;
  });
}

function addNfcPermissionToManifest(androidManifest) {
  // Add `<uses-permission android:name="android.permission.NFC" />` to the AndroidManifest.xml
  if (!Array.isArray(androidManifest.manifest["uses-permission"])) {
    androidManifest.manifest["uses-permission"] = [];
  }

  if (
    !androidManifest.manifest["uses-permission"].find(
      (item) => item.$["android:name"] === "android.permission.NFC"
    )
  ) {
    androidManifest.manifest["uses-permission"].push({
      $: {
        "android:name": "android.permission.NFC",
      },
    });
  }
  return androidManifest;
}

function addNfcHceHardwareFeatureToManifest(androidManifest) {
  // Add `<uses-feature android:name="android.hardware.nfc.hce" android:required="true" />` to the AndroidManifest.xml
  if (!Array.isArray(androidManifest.manifest["uses-feature"])) {
    androidManifest.manifest["uses-feature"] = [];
  }

  if (
    !androidManifest.manifest["uses-feature"].find(
      (item) => item.$["android:name"] === "android.hardware.nfc.hce"
    )
  ) {
    androidManifest.manifest["uses-feature"].push({
      $: {
        "android:name": "android.hardware.nfc.hce",
        "android:required": "true",
      },
    });
  }
  return androidManifest;
}

function addNfcHceServiceToManifest(androidManifest) {
  const { manifest } = androidManifest;

  if (!Array.isArray(manifest["application"])) {
    console.warn("withReactNativeHce: No manifest.application array?");
    return androidManifest;
  }

  const application = manifest["application"].find(
    (item) => item.$["android:name"] === ".MainApplication"
  );
  if (!application) {
    console.warn("withReactNativeHce: No .MainApplication?");
    return androidManifest;
  }

  if (!Array.isArray(application["service"])) {
    application["service"] = [];
  }

  if (
    !application["service"].find(
      (item) =>
        item.$["android:name"] === "com.reactnativehce.services.CardService"
    )
  ) {
    application["service"].push(NfcHceService);
  }

  return androidManifest;
}

function aidFilters(appIds) {
  return appIds.map((appId) => ({ $: { "android:name": appId } }));
}

function aidGroup(appIds) {
  return [
    {
      $: {
        "android:category": "other",
        "android:description": "@string/app_name",
      },
      "aid-filter": aidFilters(appIds),
    },
  ];
}

function hostApduService(appIds) {
  return {
    "host-apdu-service": {
      $: {
        "xmlns:android": "http://schemas.android.com/apk/res/android",
        "android:description": "@string/app_name",
        "android:requireDeviceUnlock": "false",
      },
      "aid-group": aidGroup(appIds),
    },
  };
}

function writeAidList(appIds) {
  const obj = hostApduService(appIds);
  const builder = new xml2js.Builder();
  const xml = builder.buildObject(obj);
  const dir = "android/app/src/main/res/xml";

  mkdirSync(dir, { recursive: true });
  writeFileSync(`${dir}/aid_list.xml`, xml);
}

module.exports = (config, props) =>
  withPlugins(config, [[withNfcHceAndroidManifest, props]]);

Install xml2js as a dev dependency. e.g.:

yarn add -D xml2js

Add the plugin and the application IDs to your app.json:

{
  "expo": {
    [...]
    "plugins": [
      [
        "./plugin/withReactNativeHce.js",
        {
          "appIds": [
            "D2760000850101",
            "F0010203040506",
            "F0394148148100"
          ]
        }
      ]
    ]
  }
}
1 Like

Omg Wodin, I will try that. Thank you so much in advance! Can it be implemented into expo SDK for other developers?

No, the right place for this, if it works, is in react-native-hce.

Dear Wodin,
The build was successful for me too. When I run it on Android there are not any warnings, but when I run it on iOS, it tells me:

> BlockquoteModule Hce requires main queue setup since it overrides `init` but doesn't implement `requiresMainQueueSetup`. In a future release React Native will default to initializing all native modules on a background thread unless explicitly opted-out of. 
at node_modules\react-native\Libraries\LogBox\LogBox.js:117:10 in registerWarning
at node_modules\react-native\Libraries\LogBox\LogBox.js:89:8 in RCTLog.setWarningHandler$argument_0
at node_modules\react-native\Libraries\Utilities\RCTLog.js:34:8 in logIfNoNativeHook
at node_modules\react-native\Libraries\BatchedBridge\MessageQueue.js:416:4 in __callFunction
at node_modules\react-native\Libraries\BatchedBridge\MessageQueue.js:109:6 in __guard$argument_0
at node_modules\react-native\Libraries\BatchedBridge\MessageQueue.js:364:10 in __guard
at node_modules\react-native\Libraries\BatchedBridge\MessageQueue.js:108:4 in callFunctionReturnFlushedQueue

I assume that is because iOS does not support host-based card emulation. Should I handle it somehow, or it does not a big problem. Can I leave it? And if I have to deal with this error, how could I solve this?

Another question. I created a folder as you wrote for the config plugin. Inside that folder (config-plugins) I created the "withReactNativeHce.js . After that in the root a created "app.config.json, because when I add more than two config plugins into app.json it says:
Wrong number of arguments provided for static config plugin, expected either 1 or 2, got 6
so in app.json only 1 or 2 config plugins are allowed… in app.config.json I included the plugin, as far as it similar what you suggested.

{

  "myconfig": {

    "config-plugins":

    [

      "config-plugins/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"

      }

    ],

    "android": {

      "package": "com.freeridre.senitynfcappios"

    },

    "ios": {

      "bundleIdentifier": "com.freeridre.senitynfcappios"

    }

  }

}

but when I run the app it says:

Android Bundling complete 38ms
Error: Problem validating fields in app.config.json. Learn more: https://docs.expo.dev/workflow/configuration/
 • should NOT have additional property 'myconfig'.
Error: Problem validating fields in app.config.json. Learn more: https://docs.expo.dev/workflow/configuration/
 • should NOT have additional property 'myconfig'.

I know that I use the app.config.json in a wrong way, but I have not been able to figured out how should it be configurated.
Here is a snipp of my structure of the project:

Could you help me?

I’m not sure what that’s about but it looks like it might be a warning that react-native-hce will not work properly on iOS in future versions of React Native, unless some changes are made to react-native-hce.

As for what to do about it, ideally you would figure out what it means and submit a PR to react-native-hce to fix the problem :slight_smile: but you might be able to just disable the warning. e.g. See Disable the Yellow Box in React Native

No, app.json does not limit the number of config plugins you can have. You have misinterpreted the error message. Also app.config.json is the same format as app.json.

You can’t use myconfig. It would have to be expo. And you can’t use config-plugins for key in app.json or app.config.json. It has to be plugins.

So something like this should work:

{
  "expo": {
    "name": "name",
    "slug": "slug",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "updates": {
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "plugins": [
      [
        "./config-plugins/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"
        }
      ],
      "./config-plugins/someOtherPluginWithNoOptions",
      [
        "./config-plugins/anotherPluginWithOptions",
        {
          "option": "value"
        }
      ]
    ],
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#FFFFFF"
      },
      "package": "com.example.app"
    },
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.example.app"
    }
  }
}
1 Like

Thank you so much! You are so helpful person! I glad that you help me. :slight_smile:
By th way, the build was successful again, but somehow my VS code does not find the config plugins, that I included, furthermore it does not find my assets/Images/… files, but the build was successful. What I did that I created a new folder where I put my screens for iOS and I created some other screens for Android devices. After that when I tried to include images, fonts, etc. expo or VS code did not find them, but the build was finished without errors. When I try to see my androidmanifest.xml file with expo tools extension in VS Code to check what was written inside of that, Expo-tools says:
Cannot find module 'c:\Users\dani\Documents\SenitySecuritySystemsRepo\NFCApp\SenityNFCiOS\SenityNFCAppiOS\config-plugins\withReactNativeHce.js' Source: Expo-tools (Extension)
Here is my app.json:

{
  "expo": {
    "name": "Senity",
    "slug": "SenityNFCForiOS",
    "version": "1.0.0",
    "orientation": "portrait",
    "backgroundColor": "#212832",
    "icon": "./assets/Images/Senity_app_icon_white_whit blue_v3.png",
    "splash": {
      "image": "./assets/Images/Senity_app_splash_sceen_icon.png",
      "resizeMode": "contain",
      "backgroundColor": "#212832"
    },
    "updates": {
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "supportsTablet": false,
      "requireFullScreen": true,
      "bundleIdentifier": "com.freeridre.SenityNFCForiOS",
      "splash": {
        "image": "./assets/Images/Senity_app_splash_sceen_icon.png",
        "resizeMode": "contain",
        "backgroundColor": "#212832"
      }
    },
    "android": {
      "package": "com.freeridre.SenityNFCForiOS",
      "splash": {
        "image": "./assets/Images/Senity_app_splash_sceen_icon.png",
        "resizeMode": "contain",
        "backgroundColor": "#212832"
      },
      "softwareKeyboardLayoutMode": "pan"
    },
    "web": {
      "favicon": "C:/Users/dani/Documents/SenitySecuritySystemsRepo/NFCApp/SenityNFCiOS/SenityNFCAppiOS/assets/favicon.png"
    },
    "plugins":
    [
      [
        "./config-plugins/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’m glad I could help. It’s also the first time I have written a config plugin to modify AndroidManifest.xml and the first time I have used xml2js :slight_smile:

You should change this to a relative path (./assets/…)

Otherwise if another user clones the repository and tries to build the web apo, the favicon will not be found.

I don’t use VS Code, so I am not sure why it is complaining. I use expo prebuild to test the config plugins. You just have to tidy up afterwards.

1 Like

Did it add the service into androidmanifest.xml? Because I can not see after prebuild process.

Yes, it’s true. I will change it. I found the problem. I used config-plugins or plugins for the cfg folder. After I changed it to plugin and changed the property too in app.json to “plugin” everything worked.

1 Like

Unfortunatelly, it did not work… hmm. I do not know what is the problem… So expo says:

expo-config(MODULE_NOT_FOUND)

Aprox. how much time the prebuild process to be finished?

When do you get that error?

With the following plugins definition in app.json:

    "plugins": [
      [
        "./plugin/withReactNativeHce.js",
        {
          "appIds": [
            "D2760000850101",
            "F0010203040506",
            "F0394148148100"
          ]
        }
      ]
    ]

I get the following:

android/app/src/main/res/xml/aid_list.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/app_name" android:requireDeviceUnlock="false">
  <aid-group android:category="other" android:description="@string/app_name">
    <aid-filter android:name="D2760000850101"/>
    <aid-filter android:name="F0010203040506"/>
    <aid-filter android:name="F0394148148100"/>
  </aid-group>
</host-apdu-service>

android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.rnhcetest">
  <uses-permission android:name="android.permission.INTERNET"/>
  <uses-permission android:name="android.permission.NFC"/>
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
  <uses-permission android:name="android.permission.VIBRATE"/>
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  <uses-feature android:name="android.hardware.nfc.hce" android:required="true"/>
  <queries>
    <intent>
      <action android:name="android.intent.action.VIEW"/>
      <category android:name="android.intent.category.BROWSABLE"/>
      <data android:scheme="https"/>
    </intent>
  </queries>
  <application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:usesCleartextTraffic="true">
    <meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
    <meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="43.0.0"/>
    <meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
    <meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
    <meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://exp.host/@wodin/rn-hce-test"/>
    <service android:name="com.reactnativehce.services.CardService" android:exported="true" android:enabled="false" android:permission="android.permission.BIND_NFC_SERVICE">
      <intent-filter>
        <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
        <category android:name="android.intent.category.DEFAULT"/>
      </intent-filter>
      <meta-data android:name="android.nfc.cardemulation.host_apdu_service" android:resource="@xml/aid_list"/>
    </service>
    <activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="portrait">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
      <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="com.example.rnhcetest"/>
      </intent-filter>
    </activity>
    <activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
  </application>
</manifest>

It took close to a minute to run when I tried it now and most of that was from installing the JavaScript dependencies.

After the prebuild the project looks like this:

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   .gitignore
        modified:   app.json
        modified:   package.json
        modified:   yarn.lock

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        android/
        index.js
        ios/
        metro.config.js

no changes added to commit (use "git add" and/or "git commit -a")

Since everything was committed to Git before the prebuild, I do the following to tidy up:

$ rm -r android index.js ios metro.config.js
$ git checkout .gitignore app.json package.json yarn.lock
Updated 4 paths from the index
$ yarn
yarn install v1.22.15
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@2.3.2: The platform "linux" is incompatible with this module.
info "fsevents@2.3.2" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning "react-native > react-native-codegen > jscodeshift@0.11.0" has unmet peer dependency "@babel/preset-env@^7.1.6".
[4/4] Building fresh packages...
Done in 3.32s.

Well, where I include “./plugin/withReactNativeHce.js” and “@config-plugins/react-native-ble-plx” and “react-native-nfc-manager”

But the strange thing is that after eas prebuild it generated the necessary permissions, so I guess it’s a kind of Vs code bug/error that really anoying…

We fixed the vscode extension, sorry for the inconvenience.

2 Likes