React Native Bluetooth on iOS (Nano X only) | Developers

React Native Bluetooth on iOS (Nano X only)

Estimated reading time: More than 10 minutes

Introduction

In this section, you will see how to create a React Native application using the @ledgerhq/react-native-hw-transport-ble. For this project some general prerequisites are mandatory and you can find them here.

Then you can now go through the prerequisite for iOS development below.

Prerequisites

Install Homebrew

Homebrew is a package manager for macOS. When it needs to install software from third-party websites. To install it, run:

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/uninstall)"

Install watchman

React Native uses watchman to detect real-time code changes and it automatically builds and pushes the update to your device without manually refreshing.

brew install watchman

Install Java JRE and JDK

There is a risk of react-native build failure if you don’t have a complete installation of Java. Downloading Android Studio is not enough since it comes bundled with its own JRE.

brew install --cask adoptopenjdk/openjdk/adoptopenjdk8

Install React Native

With React Native, you can write an application in Javascript and then the React Native Compiler will convert your Javascript code into native code for iOS and Android environments. React Native command line interface can be installed using npm as below.

npm install -g react-native-cli

iOS development prerequisites

To develop an iOS application we have to install Xcode via the Mac App Store.

When the installation is complete, open Xcode then go to Settings => Locations.

Xcode Settings
Fig. 1: Xcode Settings

Select the most recent version from the “Command Line Tools” dropdown.

Xcode Location Settings
Fig. 2: Xcode Location Settings

Finally, install cocoapods by running:

sudo gem install cocoapods

Mobile App Build

Now that we have set up the prerequisites, you can now create the application. In this integration, we will use the ethereum application.

Project Initialization

First, open a terminal and create a new project. For this tutorial the project will be named “ledgerApp”.

Run:

react-native init ledgerApp
cd ledgerApp
Tip
During the initialization, it is installing required 'CocoaPods' dependencies and it may take time.

Code Implementation

Run:

mkdir src
touch polyfill.js
touch src/DeviceItem.js
touch src/DeviceSelectionScreen.js
touch src/ShowAddressScreen.js

polyfill.js

In “polyfill.js”, copy-paste the following code:

global.Buffer = require("buffer").Buffer;

index.js

Then import the polyfill in “index.js” as shown below:

/**
 * @format
 */

import "./polyfill";    //import this
import {AppRegistry} from 'react-native';
import App from './src/App';    //modify this import
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => App);

App.js

Move the file named “App.js” in the “src” folder and copy-paste the following code:

import React, { Component } from "react";

import DeviceSelectionScreen from "./DeviceSelectionScreen";
import ShowAddressScreen from "./ShowAddressScreen";

import TransportBLE from "@ledgerhq/react-native-hw-transport-ble";

// This is helpful if you want to see BLE logs. (only to use in dev mode)

class App extends Component {
  state = {
    transport: null
  };

  onSelectDevice = async device => {
    const transport = await TransportBLE.open(device);
    transport.on("disconnect", () => {
      // Intentionally for the sake of simplicity we use a transport local state
      // and remove it on disconnect.
      // A better way is to pass in the device.id and handle the connection internally.
      this.setState({ transport: null });
    });
    this.setState({ transport });
  };

  render() {
    const { transport } = this.state;
    if (!transport) {
      return <DeviceSelectionScreen onSelectDevice={this.onSelectDevice} />;
    }
    return <ShowAddressScreen transport={transport} />;
  }
}

export default App;

In “DeviceItem.js” copy-paste the following code:

import React, { Component } from "react";
import {
  Text,
  TouchableOpacity,
  StyleSheet,
  ActivityIndicator
} from "react-native";

class DeviceItem extends Component {
  state = {
    pending: false
  };
  onPress = async () => {
    this.setState({ pending: true });
    try {
      await this.props.onSelect(this.props.device);
    } finally {
      this.setState({ pending: false });
    }
  };

  render() {
    const { device } = this.props;
    const { pending } = this.state;
    return (
      <TouchableOpacity
        style={styles.deviceItem}
        onPress={this.onPress}
        disabled={pending}
      >
        <Text style={styles.deviceName}>{device.name}</Text>
        {pending ? <ActivityIndicator /> : null}
      </TouchableOpacity>
    );
  }
}
export default DeviceItem;

const styles = StyleSheet.create({
  deviceItem: {
    paddingVertical: 16,
    paddingHorizontal: 32,
    marginVertical: 8,
    marginHorizontal: 16,
    borderColor: "#ccc",
    borderWidth: 1,
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between"
  },
  deviceName: {
    fontSize: 20,
    fontWeight: "bold"
  }
});

In “DeviceSelectionScreen.js”, copy-paste the following code:

import React, { Component } from "react";
import {
  StyleSheet,
  Text,
  View,
  TouchableOpacity,
  FlatList,
  Platform,
  PermissionsAndroid
} from "react-native";
import { Observable } from "rxjs";
import AppEth from "@ledgerhq/hw-app-eth";
import TransportBLE from "@ledgerhq/react-native-hw-transport-ble";
import QRCode from "react-native-qrcode-svg";
import DeviceItem from "./DeviceItem";

const deviceAddition = device => ({ devices }) => ({
  devices: devices.some(i => i.id === device.id)
    ? devices
    : devices.concat(device)
});

class DeviceSelectionScreen extends Component {
  state = {
    devices: [],
    error: null,
    refreshing: false
  };

  async componentDidMount() {
    // NB: this is the bare minimal. We recommend to implement a screen to explain to user.
    if (Platform.OS === "android") {
      await PermissionsAndroid.request(
        PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION
      );
    }
    let previousAvailable = false;
    new Observable(TransportBLE.observeState).subscribe(e => {
      if (e.available !== previousAvailable) {
        previousAvailable = e.available;
        if (e.available) {
          this.reload();
        }
      }
    });

    this.startScan();
  }

  componentWillUnmount() {
    if (this.sub) this.sub.unsubscribe();
  }

  startScan = async () => {
    this.setState({ refreshing: true });
    this.sub = new Observable(TransportBLE.listen).subscribe({
      complete: () => {
        this.setState({ refreshing: false });
      },
      next: e => {
        if (e.type === "add") {
          this.setState(deviceAddition(e.descriptor));
        }
        // NB there is no "remove" case in BLE.
      },
      error: error => {
        this.setState({ error, refreshing: false });
      }
    });
  };

  reload = async () => {
    if (this.sub) this.sub.unsubscribe();
    this.setState(
      { devices: [], error: null, refreshing: false },
      this.startScan
    );
  };

  keyExtractor = (item: *) => item.id;

  onSelectDevice = async device => {
    try {
      await this.props.onSelectDevice(device);
    } catch (error) {
      this.setState({ error });
    }
  };

  renderItem = ({ item }: { item: * }) => {
    return <DeviceItem device={item} onSelect={this.onSelectDevice} />;
  };

  ListHeader = () => {
    const { error } = this.state;
    return error ? (
      <View style={styles.header}>
        <Text style={styles.headerTitle}>Sorry, an error occured</Text>
        <Text style={styles.errorTitle}>{String(error.message)}</Text>
      </View>
    ) : (
      <View style={styles.header}>
        <Text style={styles.headerTitle}>Scanning for Bluetooth...</Text>
        <Text style={styles.headerSubtitle}>
          Power up your Ledger Nano X and enter your pin.
        </Text>
      </View>
    );
  };

  render() {
    const { devices, error, refreshing } = this.state;

    return (
      <FlatList
        extraData={error}
        style={styles.list}
        data={devices}
        renderItem={this.renderItem}
        keyExtractor={this.keyExtractor}
        ListHeaderComponent={this.ListHeader}
        onRefresh={this.reload}
        refreshing={refreshing}
      />
    );
  }
}

export default DeviceSelectionScreen;

const styles = StyleSheet.create({
  header: {
    paddingTop: 80,
    paddingBottom: 36,
    alignItems: "center"
  },
  headerTitle: {
    fontSize: 22,
    marginBottom: 16
  },
  headerSubtitle: {
    fontSize: 12,
    color: "#999"
  },
  list: {
    flex: 1
  },
  errorTitle: {
    color: "#c00",
    fontSize: 16,
    marginBottom: 16
  }
});

In “ShowAddressScreen.js”, copy-paste the following code:

import React, { Component } from "react";
import { StyleSheet, Text, View } from "react-native";

import AppEth from "@ledgerhq/hw-app-eth";
import TransportBLE from "@ledgerhq/react-native-hw-transport-ble";
import QRCode from "react-native-qrcode-svg";

const delay = ms => new Promise(success => setTimeout(success, ms));

class ShowAddressScreen extends Component {
  state = {
    error: null,
    address: null
  };

  async componentDidMount() {
    while (!this.state.address) {
      if (this.unmounted) return;
      await this.fetchAddress(false);
      await delay(500);
    }
    await this.fetchAddress(true);
  }

  async componentWillUnmount() {
    this.unmounted = true;
  }

  fetchAddress = async verify => {
    const { transport } = this.props;
    try {
      const eth = new AppEth(transport);
      const path = "44'/60'/0'/0/0"; // HD derivation path
      const { address } = await eth.getAddress(path, verify);
      if (this.unmounted) return;
      this.setState({ address });
    } catch (error) {
      // in this case, user is likely not on Ethereum app
      if (this.unmounted) return;
      this.setState({ error });
      return null;
    }
  };

  render() {
    const { address, error } = this.state;

    return (
      <View style={styles.ShowAddressScreen}>
        {!address ? (
          <>
            <Text style={styles.loading}>Loading your Ethereum address...</Text>
            {error ? (
              <Text style={styles.error}>
                A problem occurred, make sure to open the Ethereum application
                on your Ledger Nano X. (
                {String((error && error.message) || error)})
              </Text>
            ) : null}
          </>
        ) : (
          <>
            <Text style={styles.title}>Ledger Live Ethereum Account 1</Text>
            <QRCode value={address} size={300} />
            <Text style={styles.address}>{address}</Text>
          </>
        )}
      </View>
    );
  }
}

export default ShowAddressScreen;

const styles = StyleSheet.create({
  ShowAddressScreen: {
    flex: 1,
    padding: 16,
    alignItems: "center",
    justifyContent: "center"
  },
  error: {
    color: "#c00",
    fontSize: 16
  },
  loading: {
    color: "#999",
    fontSize: 16
  },
  title: {
    fontSize: 22,
    marginBottom: 16
  },
  address: {
    marginTop: 16,
    color: "#555",
    fontSize: 14
  }
});

Your folder must look like this.

Folder of the Application
Fig. 3: Folder of the Application

Dependencies Installation

Run:

npm install --save react-native-qrcode-svg
npm install --save react-native-svg
npm install --save rxjs
npm install --save @ledgerhq/react-native-hw-transport-ble
npm install --save react-native-ble-plx
npx react-native link react-native-ble-plx
npm install --save buffer
npm install --save @ledgerhq/hw-app-eth
Package What does it do?
react-native-qrcode-svg It allows you to create a QR code.
react-native-svg It is a package mandatory to use react-native-qrcode-svg.
rxjs It is a rewrite of "Reactive-Extensions/RxJS" and is the latest production-ready version of RxJS.
@ledgerhq/react-native-hw-transport-ble It provides you with all the methods to interact with your Ledger Nano X with a Bluetooth connexion.
react-native-ble-plx It scans the bluetooth devices.
Buffer
It helps you ask your Ledger device to access the ethereum address.

Package.json dependencies

Now that the dependencies are installed you can find them in the “package.js”. This is how your “package.json” has to look like.

{
  "name": "ledgerApp",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest",
    "lint": "eslint ."
  },
  "dependencies": {
    "@ledgerhq/hw-app-eth": "^6.16.2",
    "@ledgerhq/react-native-hw-transport-ble": "^6.15.0",
    "buffer": "^6.0.3",
    "react": "17.0.2",
    "react-native": "0.66.3",
    "react-native-ble-plx": "^2.0.3",
    "react-native-qrcode-svg": "^6.1.1",
    "react-native-svg": "^12.1.1",
    "rxjs": "^7.4.0"
  },
  "devDependencies": {
    "@babel/core": "^7.16.0",
    "@babel/runtime": "^7.16.3",
    "@react-native-community/eslint-config": "^3.0.1",
    "babel-jest": "^27.3.1",
    "eslint": "^8.3.0",
    "jest": "^27.3.1",
    "metro-react-native-babel-preset": "^0.66.2",
    "react-test-renderer": "17.0.2"
  },
  "jest": {
    "preset": "react-native"
  }
}

Pod Installation

Then the pod has to be installed in the “ios” folder:

cd ios/
pod install
pod update

Enable The Bluetooth Usage

Launch Xcode and open the ios folder in the “ledgerApp” folder to add the “NSBluetoothAlwaysUsageDescription” key to the “info.plist”.

Open a Project on Xcode
Fig. 4: Open a Project on Xcode

Choose the Project
Fig. 5: Choose the Project

Add NSBluetoothAlwaysUsageDescription in info.plist
Fig. 6: Add NSBluetoothAlwaysUsageDescription in info.plist

You can now test the application you have built.

Mobile App Test

The app testing will be executed on your smartphone because the Xcode emulator environment does not allow you to use either Bluetooth or USB connexion.

Tip
Please refer to the information for iOS Emulator limitation .

Application Build on Xcode

Now to build your application on your Apple device you have to connect Xcode to an Apple account as demonstrated below.

Connect Xcode to an Apple Account
Fig. 7: Connect Xcode to an Apple Account

Then connect your Apple device to your computer to build the application directly on the Apple device. If all goes well the device name will be displayed on the top of the Xcode window, all you have to do is click on the triangle icon on the top left corner to build the app on your Apple device.

Build the Application
Fig. 8: Build the Application

Trusting the Apple Development

To accept the installation of the application. You have to trust the “Apple development” on your device. Follow the steps below.

Iphone Settings Iphone Settings Iphone Settings
Iphone Settings Iphone Settings
Fig. 9: Trusting the Apple Development

You can finally test the application by launching it.

Launching the Application

When launching the application it will be displayed as shown below. You must have the Bluetooth and location activated.

Launching the Application Launching the Application Launching the Application
Fig. 10: Launching the Application

Pairing the Ledger Nano X

To pair your Ledger Nano X you must unlock it.

Nano Code Pin
Fig. 11: Nano Code Pin

Now try to pair the Ledger Nano X to your Apple device.

Pairing the Ledger Nano Pairing the Ledger Nano
Fig. 12: Pairing the Ledger Nano X

Pairing and Launching the Ethereum App on the Nano X

When pairing, the pairing code will be displayed on your Ledger Nano X to confirm.

Confirm the pairing
Fig. 13: Confirm the pairing

Embedded Application
Fig. 14: Embedded Application

Nano Run Application
Fig. 15: Nano Run Application

Now that the pairing is done, the Nano X is ready with the ethereum application. If all goes well you must see the address of your ethereum account displayed.

Address Account Displayed on Smartphone
Fig. 16: Address Account Displayed on Smartphone

Verify the Address

For security purposes, we display on your Nano X the same ethereum address for you to confirm.

Nano Verify Screen
Fig. 17: Nano Verify Screen

Nano Verify Address Screen
Fig. 18: Nano Verify Address Screen

Embedded Approve Screen
Fig. 19: Embedded Approve Screen

Congratulations you have successfully built your first application connected with Ledger on an Apple device!


Did you find this page helpful?


How would you improve this page for developers?



React Native Bluetooth on Android (Nano X only)
Node HID integration
Getting Started
Theme Features
Customization

Connect your app