ReactNativeのWebView(Web)とアプリを連携する必要があったのでメモ。
忙しい人向け
- アプリからWeb => WebViewのinjectedJavaScriptにJSを渡して値をインジェクトしてWeb側でよしなにする
- Webからアプリ => window.ReactNativeWebView.postMessage("message")で送りonMessage()で受け取る
やりたいこと
例えば決済機能等の実装において、決済画面だけは決済サービス会社が提供するものを利用したいが、値はアプリ側で計算したものをデフォルト値として渡したいケースなど。
以下のような仕様。
最近の決済APIはカード情報非保持・非通過とするためWeb画面でカード番号を入れさせるものが多い。さらに言えばexpoでEjectしないで利用できるコンポーネントもない。
Web側
Web側は普通のHTMLだとまだ簡単なのですがReactを使うことが多いのでReactを使ってみます。
準備
場所作って必要なモジュールをインストール。
create-react-app web-app
cd web-app
npm install --save bootstrap reactstrap formik yup
実装
続いて実装。
index.js
bootstrap cssの読み込みとServiceWorkerを削除している(キャッシュが効いちゃうので)。
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つかうので使ってます。
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コマンドで適切なバージョンのものをインストール。
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を利用しています。
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に移動します。またその時金額をパラメーターとして渡しています。
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に戻りの処理を書く
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;
かなり端折ってるけるけどとりあえず。