Twitter APIをExpoでデタッチせず、かつAuth0を使わず、javascriptのみで叩く

こちらは[React Nativeアドベントカレンダー](https://qiita.com/advent-calendar/2017/react_native) 17日目の記事になります。
空いていたので勝手に書いてしまってすみません。。。

# 自己紹介
こんにちは！
いつもは[適当なブログ](http://watanabeyu.blogspot.jp/)につらつらと仕事で使ったことなどを投稿しているのですが、年末だしアドベントカレンダーあるからと思ってQiitaに書いてみることにしました。
[ハムカツおじさん🤘](https://twitter.com/hmktsu)という名前でTwitterやってます。
[g&h](http://gandh.jp)という会社でエンジニアをやっていて、サーバだったりフロントだったり、アプリだったりのなんでも屋です。

# はじめに
自分はReact Nativeというか[Expo](https://expo.io)を使ってアプリを複数作っています。
ちなみに最近出した使い道がよくわからないアプリは[こちら](http://www.bad-company.jp/pinpoint)です。

Expoは便利なんですが、使っていると限界というのがそろそろ見えてきたなぁと思いつつも、割と便利なので離れづらいなぁと思っています。

で、最近の悩みがソーシャル系のAPI関連が強くないなと。
[Facebook](https://docs.expo.io/versions/latest/sdk/facebook.html)に関してはExpoのモジュールがあるんですが、Twitterなど他のものに対応をしていない状況です。
対応していないというか[Auth0](https://auth0.com)を使って構築してねという形であり、そうなると無料枠を超えてしまったらどうなってしまうんだ？と心配になってしまいます。
ちなみにFirebaseのAuthenticationにも現状対応していないので困りますね。
さらにいうとAPIクライアントが全然見つからない…

とりわけ今作っているアプリはTwitterのAPIを使うものなので、TwitterのAPIだけとりあえず叩ければ大丈夫だろうということで、**NativeModulesも使用せず、Auth0も使用しないAPIクライアント**を作ることにしました。

もちろんExpoを使っていない通常のReact Nativeプロジェクトでも動きます。

# 作ったもの
ちっちゃいですがプレビューはこちらです。
![1513930944.gif](https://qiita-image-store.s3.amazonaws.com/0/119260/4014935f-5ae7-aacd-cb2a-fe93c64639ab.gif)

ソースコードはこちら。
https://github.com/watanabeyu/react-native-simple-twitter

Expo上で動くサンプルはこちら。
https://expo.io/@watanabe_yu/react-native-simple-twitter-example

# どうやって設置するの？
## インストール
```bash
npm install react-native-simple-twitter --save
```
まずはインストールしてください。

## App.jsにてキーとシークレットをセットする
```js: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の方がわかりやすいかと。

## ログイン画面にボタンを設置する
```js:LoginScreen.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を叩けるというのは便利なんじゃないかと個人的には思っています。

使ってくれる優しい方がおりましたら、ぜひ使ってフィードバックをください。
せこせこと改善していきたいと思います。