ReactNative(expo)でアプリとWeb(View)で値をやり取りする


ReactNativeのWebView（Web）とアプリを連携する必要があったのでメモ。

## 忙しい人向け

* アプリからWeb => WebViewのinjectedJavaScriptにJSを渡して値をインジェクトしてWeb側でよしなにする
* Webからアプリ => window.ReactNativeWebView.postMessage("message")で送りonMessage()で受け取る

## やりたいこと

例えば決済機能等の実装において、決済画面だけは決済サービス会社が提供するものを利用したいが、値はアプリ側で計算したものをデフォルト値として渡したいケースなど。

以下のような仕様。

![スクリーンショット 2020-01-01 15.37.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/52b47dce-4e12-ba38-5a54-9ef9c7e94138.png "スクリーンショット 2020-01-01 15.37.16.png")

>最近の決済APIはカード情報非保持・非通過とするためWeb画面でカード番号を入れさせるものが多い。さらに言えばexpoでEjectしないで利用できるコンポーネントもない。

## Web側

Web側は普通のHTMLだとまだ簡単なのですがReactを使うことが多いのでReactを使ってみます。

### 準備


場所作って必要なモジュールをインストール。

```bash
create-react-app web-app
cd web-app

npm install --save bootstrap reactstrap formik yup
```

### 実装

続いて実装。

#### index.js

bootstrap cssの読み込みとServiceWorkerを削除している（キャッシュが効いちゃうので）。

```js:index.js
import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));
```

#### App.js

App.jsに一旦すべて実装。

* priceというidのinputを用意
* cardというidのinputを用意
* priceにinjectedJavaScriptから値を設定

ということを想定。私いつもFormikつかうので使ってます。

```js:App.js
import React from 'react';
import { Form, FormGroup, Label, Input, Button, FormFeedback } from 'reactstrap';
import { Formik } from 'formik';
import * as Yup from 'yup';

class App extends React.Component {

    handlePayment = async (values) => {

        //1秒休む
        await this.sleep(1000);

        //終了したらアプリ側にメッセージを送る
        window.ReactNativeWebView.postMessage(values.price + "円の決済が完了しました。");
    }

    //おやすみ補助関数
    sleep = (msec) => {
        return new Promise((resolve) => {
            setTimeout(() => {
                return resolve();
            }, msec)
        })
    }

    render() {
        return (
            <div className="container">
                <h3 className="my-4 text-center">Payment（ここはWeb）</h3>
                <div className="col-10 mx-auto">
                    <Formik
                        initialValues={{ price: 0, card: '1111-2222-3333-4444' }}
                        onSubmit={(values) => this.handlePayment(values)}
                        validationSchema={Yup.object().shape({
                            price: Yup.number().min(1).max(1000),
                            card: Yup.string().required(),
                        })}
                    >
                        {
                            ({ handleSubmit, handleChange, handleBlur, values, errors, touched, setFieldValue }) => (
                                <Form>
                                    <FormGroup>
                                        <Label>金額</Label>
                                        <Input
                                            type="text"
                                            name="price"
                                            id="price" //idで強引に値をセット
                                            value={values.price}
                                            onChange={handleChange}
                                            onBlur={handleBlur}
                                            invalid={Boolean(touched.price && errors.price)}
                                            disabled
                                        />
                                        <FormFeedback>
                                            {errors.price}
                                        </FormFeedback>
                                    </FormGroup>
                                    <FormGroup>
                                        <Label>カード番号</Label>
                                        <Input
                                            type="text"
                                            name="card"
                                            id="card"
                                            value={values.card}
                                            onChange={handleChange}
                                            onBlur={handleBlur}
                                            invalid={Boolean(touched.card && errors.card)}
                                        />
                                        <FormFeedback>
                                            {errors.card}
                                        </FormFeedback>
                                    </FormGroup>
                                    <Button type="button" onClick={async () => {
                                        const price = document.getElementById("price");
                                        //Formik使ってるので値を明示的にセットしてやる（完了するうちにValidationが走らないようawait）
                                        await setFieldValue("price", price.value);
                                        handleSubmit();
                                    }}>購入</Button>
                                </Form>
                            )
                        }
                    </Formik>
                </div>
            </div>
        );
    }
}

export default App;
```
とりあえず完成。window.ReactNativeWebView.postMessage()なんていう関数は標準のブラウザにはないのでchrome等でデバッグするとエラー出ますが無視します。

## アプリ側

次にアプリ側。
場所の準備と必要コンポーネントをインストール。WebViewは普通にインストールするとexpoに怒られるのでexpo installコマンドで適切なバージョンのものをインストール。

```bash
expo init app-web-integration
cd app-web-integration

expo install react-navigation react-native-gesture-handler react-native-reanimated react-native-screens
expo install react-navigation-stack react-navigation-tabs react-navigation-drawer
expo install react-native-webview
```

まずApp.js。基本的にStackNavigatorを設定しているだけ。
Home.jsとPayment.jsを利用しています。

```js:App.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Card, Input, Button } from 'react-native-elements';

import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';

import Home from './Home';
import Payment from './Payment';

//stack navigator
const HomeStack = createStackNavigator(
    {
        Home: {
            screen: Home,
        },
        Payment: {
            screen: Payment,
        }
    }
);

const AppContainer = createAppContainer(HomeStack);

class App extends React.Component {
    render() {
        return (
            <AppContainer />
        );
    }
}

export default App;
```

### Home.js

ボタンを配置してPayment.jsに移動します。またその時金額をパラメーターとして渡しています。

```js:Home.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Card, Input, Button } from 'react-native-elements';

class Home extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', marginTop: 50 }}>
                <Text style={{ fontSize: 24 }}>Home（ここはアプリ）</Text>
                <Button
                    title="100円コースを買う"
                    style={{ width: '80%', marginTop: 20 }}
                    onPress={() => this.props.navigation.navigate("Payment", { price: 100 })}
                />
                <Button
                    title="200円コースを買う"
                    style={{ width: '80%', marginTop: 20 }}
                    onPress={() => this.props.navigation.navigate("Payment", { price: 200 })}
                />
            </View>
        );
    }
}

export default Home;
```

### Payment.js

このコンポーネントはWebViewになります。WebViewの、

* injectedJavaScriptにWeb側に渡す値を設定
* onMessageに戻りの処理を書く

```js:Payment.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Card, Input, Button } from 'react-native-elements';
import { WebView } from 'react-native-webview';

class Payment extends React.Component {

    state = {
        js: '',
    }

    componentDidMount = async () => {
        //前のページからパラメータを受け取る（なければ0）
        const price = await this.props.navigation.state.params.price ? this.props.navigation.state.params.price : 0;
        //priceを設定するスクリプトを動的に生成
        const js = `
            const price = document.getElementById("price");
            price.value = ${price}
        `;
        //stateを通じて渡す
        this.setState({ js: js });
    }

    //Web側からのpostMessageに対応
    onMessage = (event) => {
        const message = event.nativeEvent.data;
        this.props.navigation.navigate("Home");
        alert(message);
    }

    render() {

        //js内の変数が処理されないうちにWebViewがレンダリングするのを防ぐ
        if (this.state.js === '') {
            return <Text>Loading...</Text>
        }

        //WebViewをレンダリング
        return (
            <WebView
                source={{ uri: 'http://localhost:3000/' }}
                injectedJavaScript={this.state.js}
                onMessage={this.onMessage}
            />
        )
    }
}

export default Payment;
```

かなり端折ってるけるけどとりあえず。