React Native + Expo アプリで QRコードを読んでJSON取得

この記事は、「[【連載】初めてのReact Native + Expo開発環境構築入門](https://qiita.com/hitotch/items/5142fff638c7805d84d5)」の子記事です。環境などの条件は、親記事をご覧ください。

---

　[前回](https://qiita.com/hitotch/items/f4f227a6ca3ca6c62e8e)までに、請求書情報をサーバーから取得して表示できるようになったので、今回は呼び出す先のサーバー情報をQRコードから取得できるようにします。

　QRコードにあらかじめJSONを仕込んでおけば、複数の設定項目がある設定をアプリの設定方法説明書なしでユーザーに実施してもらえるようになるなど、利便性アップや業務の効率化が見込めます。

**【注意】現時点で、QRスキャナ機能がうまく動作しない場合があるようです。詳しくは[フォーラム](https://github.com/expo/expo/issues/3886)参照。**

目標：Importボタンを押す　→　QRコードリーダー　→　QRに入ってるURLから請求書を読み込み
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/b4874909-ad3e-6e60-1483-5b046488d328.png)　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/732217db-6971-4836-4c9a-14cd05a7e9a6.png)　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/c5460555-2800-42b6-20e4-67f438f09fab.png)


# QRリーダーをインストール

　``BarCodeScanner``モジュールは、QRだけでなくいろんな企画のバーコードも読み込めるバーコード・QRリーダーです。さくっとインストールします。Visual Studio CodeでReact Nativeのプロジェクトを開き、``Ctrl + @``でPowerShellを開いて以下のコマンドを実行。

```
expo install expo-barcode-scanner
```

# QRコード読み込み画面を設置

　QRコード読み込み画面は、[ExpoのバーコードAPIの例](https://docs.expo.io/versions/latest/sdk/bar-code-scanner/)からコピーして、unstatedのためのHOC化を実施します。

```jsx:/components/BarcodeScannerScreen.js
import * as React from "react";
import { Text, View, StyleSheet, Button } from "react-native";
import Constants from "expo-constants";
import * as Permissions from "expo-permissions";
import { Subscribe } from "unstated";

import { BarCodeScanner } from "expo-barcode-scanner";
import InvoiceContainer from "../containers/InvoiceContainer";

class BarcodeScannerScreenContent extends React.Component {
  state = {
    hasCameraPermission: null,
    scanned: false
  };

  async componentDidMount() {
    this.getPermissionsAsync();
  }

  getPermissionsAsync = async () => {
    const { status } = await Permissions.askAsync(Permissions.CAMERA);
    this.setState({ hasCameraPermission: status === "granted" });
  };

  render() {
    const { hasCameraPermission, scanned } = this.state;

    if (hasCameraPermission === null) {
      return <Text>Requesting for camera permission</Text>;
    }
    if (hasCameraPermission === false) {
      return <Text>No access to camera</Text>;
    }
    return (
      <View
        style={{
          flex: 1,
          flexDirection: "column",
          justifyContent: "flex-end"
        }}
      >
        <BarCodeScanner
          onBarCodeScanned={scanned ? undefined : this.handleBarCodeScanned}
          style={StyleSheet.absoluteFillObject}
        />

        {scanned && <Button title={"Tap to Scan Again"} onPress={() => this.setState({ scanned: false })} />}
      </View>
    );
  }

  handleBarCodeScanned = ({ type, data }) => {
    this.setState({ scanned: true });
    // alert(`Bar code with type ${type} and data ${data} has been scanned!`);
    let jsondata = {};
    if (this.isJson(data) && (jsondata = JSON.parse(data)).invoice_endpoint_url) {
      this.props.globalState.getDataFromServer(jsondata.invoice_endpoint_url);
      this.props.navigation.goBack();
    } else {
      alert("This is not a valid QR code.");
    }
  };

  isJson(str) {
    try {
      JSON.parse(str);
    } catch (e) {
      return false;
    }
    return true;
  }
}

const BarcodeScannerScreen = ({ navigation }) => {
  return (
    <Subscribe to={[InvoiceContainer]}>
      {globalState => <BarcodeScannerScreenContent globalState={globalState} navigation={navigation} />}
    </Subscribe>
  );
};

export default BarcodeScannerScreen;
```

　unstatedとnavigationのためにHOC化以外に実施していることは、QRコードを読み込んだら中身をチェックし、もしJSONで、さらに中に``invoice_endpoint_url``があったら指定URLから請求書読み込み実施して画面を戻る、そうでなければ「This is not a valid QR code.」とアラート、です。アラートの場合、navigationのBackボタンで画面を戻ることもできるし、「Tap to Scan Again」ボタンで再挑戦することもできます。

　また、グローバルStateのInvoiceContainerのgetDataFromServer()をコールするときに、QRコードから受け取った``invoice_endpoint_url``を渡すことにしました。このため、InvoiceContainerの修正も必要になります。

　作った画面コンポーネントをnavigationに登録。

```jsx:App.js
...
import BarcodeScannerScreen from "./components/BarcodeScannerScreen";
...
const RootStack = createStackNavigator(
  {
    Home: HomeScreen,
    InvoiceEdit: InvoiceEditScreen,
    Summary: SummaryScreen,
    BarcodeScanner: BarcodeScannerScreen
  },
  {
    initialRouteName: "Home"
  }
);
...
```

# 指定されたURLから請求書を読み込む動作を実装

　請求書取得のエンドポイントはもともとINVOICE_API_ENDPOINTに定数化していましたが、引数で受け付けるように変更します。

```jsx:containers/InvoiceContainer.js
...
// const INVOICE_API_ENDPOINT = "http://192.168.1.8:8080/invoice.js";
...
  getDataFromServer(endpoint) {
    this.setState({ isDataLoading: true });
    axios
      .get(endpoint, { timeout: 3000 })
...
```

※ついでに通信タイムアウトを３秒に設定しました。

# JSONにエンドポイントURLが入ったQRコードを作成

　[オンラインQRコード生成アプリ QR Code Generator](https://www.qr-code-generator.com/)などで、以下のJSONをQRコード化。※ここで指定するURLは、[前回の記事](https://qiita.com/hitotch/items/f4f227a6ca3ca6c62e8e)の請求書JSONファイルが取れるアドレスを指定します。

```json
{"invoice_endpoint_url":"http://192.168.1.8:8080/invoice.js"}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/7cd39f16-2be2-314d-fe15-0cecdc0daa22.png)

# 請求書読み込みボタンでQRスキャン画面に飛べるようにする

　HomeScreenのImportボタンで、QRリーダー画面に飛べるようにします。（以前の動作はコメントアウト）

```jsx:components/HomeScreen.js
  onImportClick() {
    // this.props.globalState.getDataFromServer();
    this.props.navigation.navigate("BarcodeScanner");
  }
```

　コンポーネントではなくグローバルState側にサーバーとの通信動作を記述しているので、変更はとても簡単ですね。

# 結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/b4874909-ad3e-6e60-1483-5b046488d328.png)

Importボタンを押すとQRスキャン画面へ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/732217db-6971-4836-4c9a-14cd05a7e9a6.png)

間違ったQRコード（やバーコード）を読むと、警告。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/ab8db455-1e3b-c384-4bf7-b418babdfef1.png)

正しいQRを読み込むと、サーバーの請求書を読み込んで元画面に遷移。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/c5460555-2800-42b6-20e4-67f438f09fab.png)