こちらはReact Nativeアドベントカレンダー 17日目の記事になります。
空いていたので勝手に書いてしまってすみません。。。
自己紹介
こんにちは!
いつもは適当なブログにつらつらと仕事で使ったことなどを投稿しているのですが、年末だしアドベントカレンダーあるからと思ってQiitaに書いてみることにしました。
ハムカツおじさん🤘という名前でTwitterやってます。
g&hという会社でエンジニアをやっていて、サーバだったりフロントだったり、アプリだったりのなんでも屋です。
はじめに
自分はReact NativeというかExpoを使ってアプリを複数作っています。
ちなみに最近出した使い道がよくわからないアプリはこちらです。
Expoは便利なんですが、使っていると限界というのがそろそろ見えてきたなぁと思いつつも、割と便利なので離れづらいなぁと思っています。
で、最近の悩みがソーシャル系のAPI関連が強くないなと。
Facebookに関してはExpoのモジュールがあるんですが、Twitterなど他のものに対応をしていない状況です。
対応していないというかAuth0を使って構築してねという形であり、そうなると無料枠を超えてしまったらどうなってしまうんだ?と心配になってしまいます。
ちなみにFirebaseのAuthenticationにも現状対応していないので困りますね。
さらにいうとAPIクライアントが全然見つからない…
とりわけ今作っているアプリはTwitterのAPIを使うものなので、TwitterのAPIだけとりあえず叩ければ大丈夫だろうということで、NativeModulesも使用せず、Auth0も使用しないAPIクライアントを作ることにしました。
もちろんExpoを使っていない通常のReact Nativeプロジェクトでも動きます。
作ったもの
ソースコードはこちら。
https://github.com/watanabeyu/react-native-simple-twitter
Expo上で動くサンプルはこちら。
https://expo.io/@watanabe_yu/react-native-simple-twitter-example
どうやって設置するの?
インストール
npm install react-native-simple-twitter --save
まずはインストールしてください。
App.jsにてキーとシークレットをセットする
import React from 'react'
import { AppLoading, Asset, Font, Constants } from 'expo'
import Navigation from 'app/src'
/* npm */
import twitter from 'react-native-simple-twitter'
export default class App extends React.Component {
constructor(props) {
super(props)
this.state = {
isLoadingComplete: false
}
}
render() {
if (!this.state.isLoadingComplete && !this.props.skipLoadingScreen) {
return (
<AppLoading
startAsync={this._loadResourcesAsync}
onError={(error) => console.warn(error)}
onFinish={() => this.setState({ isLoadingComplete: true })}
/>
)
}
else {
return <Navigation />
}
}
_loadResourcesAsync = async () => {
return Promise.all([
Asset.loadAsync([
require('app/assets/images/icon.png'),
require('app/assets/images/ok_man.png')
]),
twitter.setConsumerKey(Constants.manifest.extra.twitter.consumerKey, Constants.manifest.extra.twitter.consumerKeySecret)
])
};
}
最初にTwitterアプリのキーとシークレットをセットしてあげます。
App.jsじゃなくても大丈夫ですが、App.jsの方がわかりやすいかと。
ログイン画面にボタンを設置する
import React from 'react'
import {
View,
Text,
Alert,
StyleSheet
} from 'react-native'
import { NavigationActions } from 'react-navigation'
import { connect } from 'react-redux'
import { Constants } from 'expo'
/* import twitter */
import twitter, { TWLoginButton } from 'react-native-simple-twitter'
@connect(
state => ({
user: state.user
})
)
export default class LoginScreen extends React.Component {
static navigationOptions = ({ navigation }) => {
const { state, setParams } = navigation
const { params = {} } = navigation.state
return {
header: null
}
}
constructor(props) {
super(props)
this.state = {
isVisible: false,
authUrl: null
}
}
async componentWillMount() {
if (this.props.user.token) {
twitter.setAccessToken(this.props.user.token, this.props.user.token_secret)
try {
const user = await twitter.get("account/verify_credentials.json", { include_entities: false, skip_status: true, include_email: true })
this.props.dispatch({ type: "USER_SET", user: user })
this.props.dispatch(NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'Home' })
]
}))
} catch (err) {
console.log(err)
}
}
}
onGetAccessToken = ({ oauth_token, oauth_token_secret }) => {
this.props.dispatch({ type: "TOKEN_SET", token: oauth_token, token_secret: oauth_token_secret })
}
onSuccess = (user) => {
this.props.dispatch({ type: "USER_SET", user: user })
Alert.alert(
"Success",
"ログインできました",
[
{
text: 'Go HomeScreen',
onPress: () => {
this.props.dispatch(NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'Home' })
]
}))
}
}
]
)
}
onClose = (e) => {
console.log("press close button")
}
onError = (err) => {
console.log(err)
}
render() {
return (
<View style={styles.container}>
<View style={styles.title}>
<Text style={styles.titleText}>Login</Text>
</View>
<TWLoginButton headerColor={Constants.manifest.primaryColor}
containerStyle={styles.loginContainer}
style={styles.loginButton}
textStyle={styles.loginButtonText}
onGetAccessToken={this.onGetAccessToken}
onSuccess={this.onSuccess}
closeText="閉じる"
closeTextStyle={styles.loginCloseText}
onClose={this.onClose}
onError={this.onError}>Twitter IDではじめる</TWLoginButton>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: Constants.manifest.primaryColor
},
title: {
flex: 1,
padding: 64
},
titleText: {
textAlign: "center",
fontSize: 24,
color: "#fff",
fontWeight: "bold"
},
loginContainer: {
paddingHorizontal: 32,
marginBottom: 64,
backgroundColor: "transparent"
},
loginButton: {
backgroundColor: "#fff",
paddingVertical: 16,
borderRadius: 64,
overflow: "hidden"
},
loginButtonText: {
color: Constants.manifest.primaryColor,
fontSize: 16,
fontWeight: "bold",
textAlign: "center"
},
loginCloseText: {
color: "#fff",
fontWeight: "bold"
}
})
そしてTWLoginButton
を設置してあげれば、ツイッターログインができます。
ちなみにトークンを取得した際、およびログインが成功した際にコールバックを設定することができるので、その中でreduxに投げてあげるということもできます。
なおこのクライアント自体シングルトンで作っているので、ログインしたらトークンはアプリ自体が切れるまでは有効です。
なのでトークンを取得した際にAsyncStorageに保存しておくとかして、アプリ立ち上げ時にAsyncStorageからトークンを取得していつでもAPIを叩ける状況にしておくことも可能です。
サンプルURLはこちらです。
https://github.com/watanabeyu/react-native-simple-twitter/tree/master/example
ライブラリ構成は下記になります。
- react-navigation
- react-redux
- redux
苦労したところ
OAuth1.0のsignatureを作るのがなかなか面倒でした。
パラメーターを全てエンコードして文字列にして、hashしてbase64をし、さらに云々という形で地味に大変だなぁと。
文字列にする際にパラメーターのkeyの順番が影響してきたり、さらに半角スペースや他の文字のエンコードもちゃんとしてあげないとダメでした。
まとめ
つらつらと自分の作ったAPIクライアントおよびコンポーネントの紹介となってしまいましたが、何かを介せずにTwitterのAPIを叩けるというのは便利なんじゃないかと個人的には思っています。
使ってくれる優しい方がおりましたら、ぜひ使ってフィードバックをください。
せこせこと改善していきたいと思います。