├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── __tests__ └── index.test.ts ├── babel.config.js ├── example ├── .eslintignore ├── .eslintrc ├── .vscode │ └── settings.json ├── .watchmanconfig ├── App.tsx ├── README.md ├── app.json ├── assets │ └── images │ │ ├── icon.png │ │ └── ok_man.png ├── babel.config.js ├── package.json ├── src │ ├── index.tsx │ ├── navigation │ │ └── AppNavigator.tsx │ └── screens │ │ ├── HomeScreen.tsx │ │ └── LoginScreen.tsx └── tsconfig.json ├── extras └── demo.gif ├── package.json ├── src ├── Components │ ├── Button.tsx │ ├── Header.tsx │ └── Modal.tsx ├── client.ts ├── error.ts ├── hooks.tsx ├── index.ts ├── lib.ts ├── request.ts ├── types.ts └── util.ts └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | working_directory: ~/repo 5 | docker: 6 | - image: circleci/node:latest 7 | 8 | jobs: 9 | test: 10 | <<: *defaults 11 | steps: 12 | - checkout 13 | 14 | - restore_cache: 15 | keys: 16 | - dependencies-{{ checksum "package.json" }} 17 | - dependencies- 18 | 19 | - run: npm install 20 | - run: 21 | name: Run tests 22 | command: npm run test 23 | 24 | - save_cache: 25 | paths: 26 | - node_modules 27 | key: dependencies-{{ checksum "package.json" }} 28 | 29 | - persist_to_workspace: 30 | root: ~/repo 31 | paths: . 32 | build: 33 | <<: *defaults 34 | steps: 35 | - attach_workspace: 36 | at: ~/repo 37 | - run: npm install 38 | - run: 39 | name: build script 40 | command: npm run build 41 | deploy: 42 | <<: *defaults 43 | steps: 44 | - attach_workspace: 45 | at: ~/repo 46 | - run: npm install 47 | - run: 48 | name: build script 49 | command: npm run build 50 | - run: 51 | name: Authenticate with registry 52 | command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc 53 | - run: 54 | name: Publish package 55 | command: npm publish 56 | 57 | workflows: 58 | version: 2 59 | test-deploy: 60 | jobs: 61 | - test: 62 | filters: 63 | tags: 64 | only: /^v.*/ 65 | - build: 66 | requires: 67 | - test 68 | - deploy: 69 | requires: 70 | - test 71 | - build 72 | filters: 73 | tags: 74 | only: /^v.*/ 75 | branches: 76 | ignore: /.*/ -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | types/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parser": "@typescript-eslint/parser", 3 | "env": { 4 | "es6": true, 5 | "browser": true, 6 | "jest": true 7 | }, 8 | "extends": [ 9 | "airbnb", 10 | ], 11 | "globals": { 12 | "__DEV__": true 13 | }, 14 | "plugins": [ 15 | "@typescript-eslint" 16 | ], 17 | "settings": { 18 | "import/extensions": [ 19 | ".js", 20 | ".jsx", 21 | ".ts", 22 | ".tsx" 23 | ], 24 | "import/resolver": { 25 | "node": { 26 | "extensions": [ 27 | ".js", 28 | ".jsx", 29 | ".ts", 30 | ".tsx" 31 | ] 32 | } 33 | } 34 | }, 35 | "rules": { 36 | camelcase: [ 37 | "error", 38 | { 39 | allow: [ 40 | "^oauth_", 41 | "^include_", 42 | "^default_profile", 43 | "^profile_", 44 | "^withheld_", 45 | "_count$", 46 | "_url$", 47 | "skip_status", 48 | "screen_name", 49 | "user_id", 50 | "created_at", 51 | "id_str" 52 | ] 53 | } 54 | ], 55 | "no-unused-vars": "off", 56 | "@typescript-eslint/no-unused-vars": "error", 57 | 'react/jsx-props-no-spreading': ['error', { 58 | html: 'enforce', 59 | custom: 'ignore', 60 | exceptions: [], 61 | }], 62 | "max-len": [ 63 | 1, 64 | 140, 65 | 2 66 | ], 67 | "jsx-a11y/label-has-for": [2, { 68 | "required": { 69 | "some": ["nesting", "id"] 70 | } 71 | }], 72 | "react/prop-types": [ 73 | 0 74 | ], 75 | "react/destructuring-assignment": 0, 76 | "react/jsx-one-expression-per-line": [ 77 | 0, 78 | { 79 | "allow": "literal" 80 | } 81 | ], 82 | "import/no-extraneous-dependencies": [ 83 | "error", 84 | { 85 | "devDependencies": ["src/**", "__tests__/**"], 86 | "optionalDependencies": false, 87 | "peerDependencies": false 88 | } 89 | ], 90 | "react/jsx-filename-extension": [ 91 | "error", 92 | { 93 | "extensions": [ 94 | ".js", 95 | ".jsx", 96 | ".ts", 97 | ".tsx" 98 | ] 99 | } 100 | ], 101 | "import/extensions": [ 102 | "error", "always", 103 | { 104 | "js": "never", 105 | "jsx": "never", 106 | "ts": "never", 107 | "tsx": "never" 108 | } 109 | ] 110 | } 111 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: watanabeyu -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .expo 3 | package-lock.json 4 | dist/ 5 | types/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | example 3 | node_modules 4 | extras 5 | src 6 | __tests__ 7 | .gitignore 8 | .npmignore 9 | .eslintignore 10 | .eslintrc.js 11 | .vscode 12 | .circleci 13 | tsconfig.json 14 | tsconfig.types.json 15 | babel.config.js -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.nodePath": "./node_modules/eslint", 3 | "eslint.run": "onSave", 4 | "eslint.alwaysShowStatus": true, 5 | "eslint.enable": true, 6 | "eslint.validate": [ 7 | "javascript", 8 | "javascriptreact", 9 | "typescript", 10 | "typescriptreact" 11 | ], 12 | "typescript.tsdk": "node_modules/typescript/lib", 13 | "editor.codeActionsOnSave": { 14 | "source.fixAll.eslint": true 15 | } 16 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## v3.0.0 - 2019-12-18 4 | ### Added 5 | - custom hook add `useTwitter` 6 | - api `Method` type 7 | - try catch error 8 | - custom error 9 | 10 | ### Changed 11 | - types 12 | 13 | ### Removed 14 | - api's method to UpperCase() 15 | 16 | ### Will be removed next version 17 | - `twitter.get()` 18 | - `twitter.post()` 19 | - `TWLoginButton` 20 | 21 | ## v2.4.0 - 2019-10-16 22 | ### Added 23 | - TWLoginButton add `props.closeText` 24 | 25 | ### Changed 26 | - Class component and stateless functional component -> **hooks** component 27 | - TWLoginButton `props.renderHeader` argument 28 | - react-native-safearea-view -> `react-native`'s SafeAreaView; 29 | - WebView -> `react-native-webview` -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Yu Watanabe - http://www.bad-company.jp - fight.ippatu5648@gmail.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **If you have request, Please send a PR or issue.** 2 | * please see [CHANGELOG.md](CHANGELOG.md) 3 | * `TWLoginButton` / `twitter.get()` / `twitter.post()` will be removed on next version (v3.1.0). 4 | 5 | # React-Native-Simple-Twitter v3.0 6 | Twitter API client for React Native without `react-native link`. 7 | This package **don't use NativeModule**, only pure javascript. 8 | So don't need to use `react-native link` and [Expo](https://expo.io) can also easily use twitter API without auth0 and server. 9 | 10 | You can use custom hooks from v3.0 11 | 12 | ``` 13 | ... 14 | import { useTwitter } from "react-native-simple-twitter"; 15 | 16 | function Login() { 17 | // if login, please set onSuccess 18 | const { twitter,TWModal } = useTwitter({ 19 | onSuccess:(user,accessToken) => { 20 | console.log(user); 21 | console.log(accessToken); 22 | } 23 | }); 24 | 25 | const onLoginPress = async () => { 26 | try { 27 | await twitter.login(); 28 | } catch(e) { 29 | console.log(e.errors); 30 | } 31 | } 32 | 33 | useEffect(() => { 34 | twitter.setConsumerKey("key","secret"); 35 | },[]); 36 | 37 | ... 38 | 39 | return ( 40 | 41 | login 42 | 43 | 44 | ) 45 | } 46 | ``` 47 | 48 | Checkout v3.x [example](example). 49 | 50 | Previous version -> [v2.4.1](https://github.com/watanabeyu/react-native-simple-twitter/tree/2759e423db803d31f50bdb24adcabbf43afd925d) 51 | 52 | ## Installation 53 | This package use WebView, but WebView from react-native is deprecated, so you download with `react-native-webview`. 54 | ```bash 55 | $ npm install react-native-simple-twitter react-native-webview --save 56 | ``` 57 | 58 | if you want to use more twitter types, use [abraham/twitter-d](https://github.com/abraham/twitter-d) 59 | ```bash 60 | $ npm install --save-dev twitter-d 61 | ``` 62 | 63 | ## Demo 64 | ![demo gif](extras/demo.gif) 65 | 66 | ## useTwitter API 67 | ``` 68 | import { useTwitter } from 'react-native-simple-twitter'; 69 | 70 | const { twitter, TWModal } = useTwitter({ 71 | onSuccess:(user,accessToken) => void, 72 | onError?:(err) => void, 73 | }) 74 | ``` 75 | 76 | ### useTwitter() 77 | | Name | Description | 78 | | --- | --- | 79 | | `onSuccess:(user,accessToken) => void` | return loggedin user object and access token | 80 | | `onError?:(err) => void` | if login failed, call this method | 81 | 82 | ### twitter 83 | | Name | Description | 84 | | --- | --- | 85 | | `twitter.login()` | Get login url and open TWModal | 86 | | `twitter.setConsumerKey(consumer_key,consumer_key_secret)` | set application key and secret | 87 | | `twitter.getAccessToken()` | get access_token and access_token_secret, when user logged in app | 88 | | `twitter.setAccessToken(access_token,access_token_secret)` | set user access_token and access_token_secret, when you already have access_token and access_token_secret | 89 | | `twitter.api("GET" | "POST" | "PUT" | "DELETE" | "PATCH",endpoint,parameters?)` | call twitter api | 90 | | `twitter.get(endpoint,parameters)` | alias of `twitter.api`. **this method will be deprecated** | 91 | | `twitter.post(endpoint,parameters)` | alias of `twitter.api`. **this method will be deprecated** | 92 | 93 | ## Other API 94 | 95 | * decodeHTMLEntities 96 | ```js 97 | import { decodeHTMLEntities } from 'react-native-simple-twitter' 98 | 99 | console.log(decodeHTMLEntities("& ' ' / ' / < >   "")) 100 | ``` 101 | Tweet is include htmlencoded characters. 102 | So this function decode special characters. 103 | 104 | * getRelativeTime 105 | ```js 106 | import { getRelativeTime } from 'react-native-simple-twitter' 107 | 108 | console.log(getRelativeTime(new Date(new Date().getTime() - 32390))) 109 | console.log(getRelativeTime("Thu Apr 06 15:28:43 +0000 2017")) 110 | ``` 111 | Tweet created_at convert to relative time. 112 | ex) 1s 15m 23h -------------------------------------------------------------------------------- /__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as util from '../src/util'; 2 | import * as lib from '../src/lib'; 3 | 4 | describe('util test', () => { 5 | it('util.randomStrings()', () => { 6 | const result = util.randomStrings(); 7 | 8 | expect(result).toMatch(/^[a-zA-Z0-9]+$/); 9 | }); 10 | 11 | it('util.createHeaderString()', () => { 12 | const result = util.createHeaderString({ 13 | a: 'a', b: 'hoge', 0: '2', aa: 111, 14 | }); 15 | 16 | expect(result).toBe('OAuth 0=2, a=a, aa=111, b=hoge'); 17 | }); 18 | 19 | it('util.encodeParamsToString()', () => { 20 | const result = util.encodeParamsToString({ 21 | a: 'a', b: 'hoge', 0: '2', aa: 111, 22 | }); 23 | 24 | expect(result).toBe('0=2&a=a&aa=111&b=hoge'); 25 | }); 26 | 27 | it('util.parseFormEncoding()', () => { 28 | const result = util.parseFormEncoding('0=2&a=a&aa=111&b=hoge'); 29 | 30 | expect(result).toHaveProperty('0', '2'); 31 | expect(result).toHaveProperty('a', 'a'); 32 | expect(result).toHaveProperty('aa', '111'); 33 | expect(result).toHaveProperty('b', 'hoge'); 34 | }); 35 | 36 | it('util.createTokenRequestHeaderParams()', () => { 37 | const result = util.createTokenRequestHeaderParams('consumerKey', { callback: 'https://github.com', token: 'token', params: { a: 1 } }); 38 | 39 | expect(result).toHaveProperty('oauth_callback', 'https://github.com'); 40 | expect(result).toHaveProperty('oauth_consumer_key', 'consumerKey'); 41 | expect(result).toHaveProperty('oauth_nonce'); 42 | expect(result).toHaveProperty('oauth_signature_method', 'HMAC-SHA1'); 43 | expect(result).toHaveProperty('oauth_timestamp'); 44 | expect(result).toHaveProperty('oauth_version', '1.0'); 45 | expect(result).toHaveProperty('oauth_token', 'token'); 46 | expect(result).toHaveProperty('a', 1); 47 | }); 48 | 49 | it('util.createSignature()', () => { 50 | const result = util.createSignature({ a: 'b' }, 'POST', 'https://github.com', 'consumerSecret', 'tokenSecret'); 51 | 52 | expect(result).toHaveProperty('oauth_signature', 'chnF4YlwsX2XpMpZGJKgtsXdAkc='); 53 | expect(result).toHaveProperty('a', 'b'); 54 | }); 55 | }); 56 | 57 | describe('lib test', () => { 58 | it('lib.decodeHTMLEntities()', () => { 59 | const result = lib.decodeHTMLEntities('& ' ' / ' / < >   "'); 60 | 61 | expect(result).toBe('& \' \' / \' / < > "'); 62 | }); 63 | 64 | it('lib.getRelativeTime()', () => { 65 | const result1 = lib.getRelativeTime(new Date(new Date().getTime() - 32390)); 66 | const result2 = lib.getRelativeTime('Thu Apr 06 15:28:43 +0900 2017'); 67 | 68 | expect(result1).toBe('32s'); 69 | expect(result2).toBe('2017/04/06'); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | env: { 13 | test: { 14 | plugins: [ 15 | '@babel/plugin-proposal-class-properties', 16 | ], 17 | }, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /example/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/ -------------------------------------------------------------------------------- /example/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "es6": true, 5 | "browser": true 6 | }, 7 | "extends": "airbnb", 8 | "globals": { 9 | "__DEV__": true 10 | }, 11 | "rules": { 12 | "max-len": [ 13 | 1, 14 | 140, 15 | 2 16 | ], 17 | "react/prop-types": [ 18 | 0 19 | ], 20 | "react/destructuring-assignment": 0, 21 | "import/no-unresolved": [ 22 | 2, 23 | { 24 | "ignore": [ 25 | "^app\/.+$" 26 | ] 27 | } 28 | ], 29 | "react/jsx-filename-extension": [ 30 | "error", 31 | { 32 | "extensions": [ 33 | ".js", 34 | ".jsx", 35 | ".ts", 36 | ".tsx" 37 | ] 38 | } 39 | ] 40 | }, 41 | "settings": { 42 | "import/core-modules": [ 43 | "app" 44 | ] 45 | } 46 | } -------------------------------------------------------------------------------- /example/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.nodePath": "./node_modules/eslint", 3 | "eslint.run": "onSave", 4 | "eslint.autoFixOnSave": true, 5 | "eslint.alwaysShowStatus": true, 6 | "eslint.enable": true, 7 | } -------------------------------------------------------------------------------- /example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { AppLoading } from 'expo'; 3 | import Constants from 'expo-constants'; 4 | import Navigation from 'app/src'; 5 | 6 | /* npm */ 7 | import twitter from 'react-native-simple-twitter'; 8 | 9 | type Props = { 10 | skipLoadingScreen: boolean, 11 | } 12 | 13 | function App(props: Props) { 14 | const [isLoadingComplete, setLoadingComplete] = useState(false); 15 | 16 | const loadResourcesAsync = async () => Promise.all([ 17 | twitter.setConsumerKey(Constants.manifest.extra.twitter.consumerKey, Constants.manifest.extra.twitter.consumerKeySecret), 18 | ]); 19 | 20 | if (!isLoadingComplete && !props.skipLoadingScreen) { 21 | return ( 22 | console.warn(error)} 25 | onFinish={() => setLoadingComplete(true)} 26 | /> 27 | ); 28 | } 29 | 30 | return ; 31 | } 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # react-native-simple-twitter example 2 | Open expo! 3 | 4 | ## Installation 5 | ```bash 6 | npm install 7 | ``` -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "react-native-simple-twitter-example", 4 | "description": "example of react-native-simple-twitter.", 5 | "slug": "react-native-simple-twitter-example", 6 | "privacy": "public", 7 | "sdkVersion": "36.0.0", 8 | "platforms": [ 9 | "ios", 10 | "android" 11 | ], 12 | "primaryColor": "#4CAF50", 13 | "extra": { 14 | "twitter": { 15 | "consumerKey": "sIaBRZBbihUabSRkENZUjWL2e", 16 | "consumerKeySecret": "u6qePkEENXuI5zttzFMYzIL335JBAGq38kpF8FGzptVgBt1CvB" 17 | } 18 | }, 19 | "version": "1.0.0", 20 | "orientation": "portrait", 21 | "icon": "./assets/images/icon.png", 22 | "loading": { 23 | "icon": "./assets/images/icon.png", 24 | "backgroundColor": "#ffffff" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /example/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/watanabeyu/react-native-simple-twitter/f098ddd7c75656fb7d51f02b8a6c5519dbf116d2/example/assets/images/icon.png -------------------------------------------------------------------------------- /example/assets/images/ok_man.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/watanabeyu/react-native-simple-twitter/f098ddd7c75656fb7d51f02b8a6c5519dbf116d2/example/assets/images/ok_man.png -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo', 5 | ], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "main": "node_modules/expo/AppEntry.js", 4 | "private": true, 5 | "scripts": { 6 | "test": "node ./node_modules/jest/bin/jest.js --watch" 7 | }, 8 | "jest": { 9 | "preset": "jest-expo" 10 | }, 11 | "dependencies": { 12 | "expo": "~36.0.0", 13 | "expo-constants": "^7.0.0", 14 | "react": "~16.9.0", 15 | "react-native": "https://github.com/expo/react-native/archive/sdk-36.0.0.tar.gz", 16 | "react-native-gesture-handler": "~1.5.0", 17 | "react-native-reanimated": "~1.4.0", 18 | "react-native-screens": "2.0.0-alpha.12", 19 | "react-native-simple-twitter": "^3.0.0", 20 | "react-native-webview": "^8.0.1", 21 | "react-navigation": "^4.0.10", 22 | "react-navigation-stack": "~1.10.3" 23 | }, 24 | "devDependencies": { 25 | "babel-eslint": "^10.0.1", 26 | "babel-plugin-module-resolver": "^3.2.0", 27 | "babel-preset-expo": "^5.0.0", 28 | "eslint": "^5.15.1", 29 | "eslint-config-airbnb": "^17.1.0", 30 | "eslint-plugin-import": "^2.16.0", 31 | "eslint-plugin-jsx-a11y": "^6.2.1", 32 | "eslint-plugin-react": "^7.12.4", 33 | "jest-expo": "^32.0.0", 34 | "schedule": "^0.4.0", 35 | "typescript": "^3.7.3" 36 | } 37 | } -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | StatusBar, 4 | View, 5 | Platform, 6 | } from 'react-native'; 7 | 8 | import AppWithNavigationState from 'app/src/navigation/AppNavigator'; 9 | 10 | function Navigation() { 11 | return ( 12 | 13 | {Platform.OS === 'ios' && } 14 | {Platform.OS === 'android' && } 15 | 16 | 17 | ); 18 | } 19 | 20 | export default Navigation; 21 | -------------------------------------------------------------------------------- /example/src/navigation/AppNavigator.tsx: -------------------------------------------------------------------------------- 1 | import { createAppContainer } from 'react-navigation'; 2 | import { createStackNavigator } from 'react-navigation-stack'; 3 | 4 | /* screen */ 5 | import LoginScreen from 'app/src/screens/LoginScreen'; 6 | import HomeScreen from 'app/src/screens/HomeScreen'; 7 | 8 | /* AppNavigator */ 9 | const AppNavigator = createStackNavigator( 10 | { 11 | Login: { screen: LoginScreen }, 12 | Home: { screen: HomeScreen }, 13 | }, 14 | { 15 | initialRouteName: 'Login', 16 | }, 17 | ); 18 | 19 | export default createAppContainer(AppNavigator); 20 | -------------------------------------------------------------------------------- /example/src/screens/HomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | Image, 6 | TouchableOpacity, 7 | Alert, 8 | AsyncStorage, 9 | StyleSheet, 10 | } from 'react-native'; 11 | 12 | /* import twitter */ 13 | import { useTwitter } from 'react-native-simple-twitter'; 14 | 15 | /* img */ 16 | const okImg = require('app/assets/images/ok_man.png'); 17 | 18 | const styles = StyleSheet.create({ 19 | container: { 20 | flex: 1, 21 | }, 22 | content: { 23 | flex: 1, 24 | padding: 32, 25 | }, 26 | name: { 27 | textAlign: 'center', 28 | fontSize: 16, 29 | fontWeight: 'bold', 30 | marginBottom: 16, 31 | }, 32 | description: { 33 | fontSize: 16, 34 | marginBottom: 16, 35 | }, 36 | button: { 37 | backgroundColor: '#1da1f2', 38 | paddingVertical: 16, 39 | }, 40 | buttonText: { 41 | textAlign: 'center', 42 | fontSize: 16, 43 | color: '#fff', 44 | }, 45 | }); 46 | 47 | function HomeScreen(props) { 48 | const { twitter } = useTwitter(); 49 | const [user, setUser] = useState(null); 50 | 51 | const onButtonPress = async () => { 52 | try { 53 | await twitter.api('POST', 'statuses/update.json', { status: 'テストツイート!(Test Tweet!)' }); 54 | 55 | Alert.alert( 56 | 'Success', 57 | 'ツイートできました', 58 | [ 59 | { 60 | text: 'ok', 61 | onPress: () => console.log('ok'), 62 | }, 63 | ], 64 | ); 65 | } catch (e) { 66 | console.warn(e); 67 | } 68 | }; 69 | 70 | const onLogoutButtonPress = async () => { 71 | await AsyncStorage.removeItem('user'); 72 | await AsyncStorage.removeItem('token'); 73 | twitter.setAccessToken(null, null); 74 | props.navigation.replace('Login'); 75 | }; 76 | 77 | useEffect(() => { 78 | setUser(props.navigation.getParam('user', null)); 79 | }, []); 80 | 81 | if (!user) { 82 | return null; 83 | } 84 | 85 | return ( 86 | 87 | 88 | 89 | 90 | 91 | {`${user.name} @${user.screen_name}`} 92 | {user.description} 93 | 94 | Tweetする 95 | 96 | 97 | ログアウトする 98 | 99 | 100 | 101 | ); 102 | } 103 | 104 | HomeScreen.navigationOptions = { 105 | headerTitle: 'ホーム', 106 | }; 107 | 108 | export default HomeScreen; 109 | -------------------------------------------------------------------------------- /example/src/screens/LoginScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | View, 4 | TouchableOpacity, 5 | Text, 6 | Alert, 7 | StyleSheet, 8 | AsyncStorage, 9 | } from 'react-native'; 10 | 11 | /* import twitter */ 12 | import { useTwitter, decodeHTMLEntities, getRelativeTime } from 'react-native-simple-twitter'; 13 | 14 | const styles = StyleSheet.create({ 15 | container: { 16 | flex: 1, 17 | backgroundColor: '#4CAF50', 18 | }, 19 | title: { 20 | flex: 1, 21 | padding: 64, 22 | }, 23 | titleText: { 24 | textAlign: 'center', 25 | fontSize: 24, 26 | color: '#fff', 27 | fontWeight: 'bold', 28 | }, 29 | }); 30 | 31 | function LoginScreen(props) { 32 | const [me, setMe] = useState({}); 33 | const [token, setToken] = useState<{ oauth_token: string, oauth_token_secret: string }>({ oauth_token: null, oauth_token_secret: null }); 34 | const { twitter, TWModal } = useTwitter({ 35 | onSuccess: (user, accessToken) => { 36 | setMe(user); 37 | setToken(accessToken); 38 | }, 39 | }); 40 | 41 | const onLoginPress = async () => { 42 | try { 43 | await twitter.login(); 44 | } catch (e) { 45 | console.log(e.errors); 46 | } 47 | }; 48 | 49 | useEffect(() => { 50 | console.log(decodeHTMLEntities('& ' ' / ' / < >   "')); 51 | console.log(getRelativeTime(new Date(new Date().getTime() - 32390))); 52 | console.log(getRelativeTime('Thu Apr 06 15:28:43 +0000 2017')); 53 | 54 | /* check AsyncStorage */ 55 | AsyncStorage.getItem('token').then(async (accessToken) => { 56 | if (accessToken !== null) { 57 | const userToken = JSON.parse(accessToken); 58 | twitter.setAccessToken(userToken.oauth_token, userToken.oauth_token_secret); 59 | 60 | const options = { 61 | include_entities: false, 62 | skip_status: true, 63 | include_email: true, 64 | }; 65 | 66 | try { 67 | const response = await twitter.api('GET', 'account/verify_credentials.json', options); 68 | 69 | props.navigation.replace('Home', { user: response }); 70 | } catch (e) { 71 | console.log(e.errors); 72 | } 73 | } 74 | }); 75 | }, []); 76 | 77 | useEffect(() => { 78 | if (token.oauth_token && token.oauth_token_secret && me) { 79 | const saveToAsyncStorage = async () => { 80 | await AsyncStorage.setItem('token', JSON.stringify(token)); 81 | await AsyncStorage.setItem('user', JSON.stringify(me)); 82 | 83 | Alert.alert( 84 | 'Success', 85 | 'ログインできました', 86 | [ 87 | { 88 | text: 'Go HomeScreen', 89 | onPress: () => { 90 | props.navigation.replace('Home', { user: me }); 91 | }, 92 | }, 93 | ], 94 | ); 95 | }; 96 | 97 | saveToAsyncStorage(); 98 | } 99 | }, [token]); 100 | 101 | return ( 102 | 103 | 104 | Login 105 | 106 | 107 | 108 | Twitterでログインする 109 | 110 | 111 | 112 | 113 | ); 114 | } 115 | 116 | LoginScreen.navigationOptions = { 117 | header: null, 118 | }; 119 | 120 | export default LoginScreen; 121 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "jsx": "react-native", 5 | "lib": [ 6 | "dom", 7 | "esnext" 8 | ], 9 | "moduleResolution": "node", 10 | "noEmit": true, 11 | "skipLibCheck": true, 12 | "resolveJsonModule": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "app/*": [ 16 | "*" 17 | ] 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /extras/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/watanabeyu/react-native-simple-twitter/f098ddd7c75656fb7d51f02b8a6c5519dbf116d2/extras/demo.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-simple-twitter", 3 | "version": "3.0.4", 4 | "description": "Twitter API client for React Native without react-native link", 5 | "main": "dist/index.js", 6 | "types": "types/index.d.ts", 7 | "scripts": { 8 | "build": "tsc --project tsconfig.json", 9 | "prepare": "npm run build", 10 | "test": "jest" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:watanabeyu/react-native-simple-twitter.git" 15 | }, 16 | "keywords": [ 17 | "react-native", 18 | "expo", 19 | "twitter" 20 | ], 21 | "author": "Yu Watanabe (https://github.com/watanabeyu)", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/watanabeyu/react-native-simple-twitter/issues" 25 | }, 26 | "homepage": "https://github.com/watanabeyu/react-native-simple-twitter", 27 | "dependencies": { 28 | "crypto-js": "^3.1.9-1", 29 | "react-native-webview": "^11.2.3" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.7.5", 33 | "@babel/plugin-proposal-class-properties": "^7.7.4", 34 | "@babel/preset-env": "^7.7.6", 35 | "@types/crypto-js": "^3.1.43", 36 | "@types/jest": "^24.0.23", 37 | "@types/react": "^16.9.16", 38 | "@types/react-native": "^0.60.25", 39 | "@typescript-eslint/eslint-plugin": "^2.12.0", 40 | "@typescript-eslint/parser": "^2.12.0", 41 | "babel-eslint": "^10.0.3", 42 | "babel-jest": "^24.9.0", 43 | "eslint": "^6.7.2", 44 | "eslint-config-airbnb": "^18.0.1", 45 | "eslint-plugin-import": "^2.19.1", 46 | "eslint-plugin-jsx-a11y": "^6.2.3", 47 | "eslint-plugin-react": "^7.17.0", 48 | "jest": "^24.9.0", 49 | "react": "^16.9.6", 50 | "react-native": "^0.59.8", 51 | "ts-jest": "^24.2.0", 52 | "typescript": "^3.7.3" 53 | }, 54 | "peerDependencies": { 55 | "react-native-webview": "^11.2.3" 56 | }, 57 | "jest": { 58 | "preset": "react-native", 59 | "moduleFileExtensions": [ 60 | "ts", 61 | "tsx", 62 | "js" 63 | ], 64 | "transform": { 65 | "^.+\\.(ts|tsx)$": "ts-jest" 66 | }, 67 | "globals": { 68 | "ts-jest": { 69 | "tsConfig": "tsconfig.json", 70 | "diagnostics": false 71 | } 72 | }, 73 | "testMatch": [ 74 | "**/__tests__/**/*.test.(ts|tsx|js)" 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | TouchableOpacity, 4 | TouchableHighlight, 5 | TouchableWithoutFeedback, 6 | Modal, 7 | SafeAreaView, 8 | } from 'react-native'; 9 | 10 | /* npm */ 11 | import WebView from 'react-native-webview'; 12 | 13 | /* components */ 14 | import Header from './Header'; 15 | 16 | /* client */ 17 | import twitter from '../client'; 18 | 19 | import { ErrorResponse, AccessToken, TwitterUser } from '../types'; 20 | 21 | type Props = { 22 | type: 'TouchableOpacity' | 'TouchableHighlight' | 'TouchableWithoutFeedback'; 23 | headerColor: string; 24 | callbackUrl: string; 25 | closeText: string; 26 | onPress: (e: any) => void; 27 | onGetAccessToken: (token: AccessToken) => void; 28 | onClose: (e: any) => void; 29 | onSuccess: (user: TwitterUser) => void; 30 | onError: (e: ErrorResponse) => void; 31 | renderHeader: (props: any) => React.ReactElement<{}>; 32 | children: any; 33 | } 34 | 35 | function TWLoginButton(props: Props) { 36 | const [isVisible, setVisible] = useState(false); 37 | const [authURL, setAuthURL] = useState(''); 38 | const [token, setToken] = useState({ oauth_token: '', oauth_token_secret: '' }); 39 | 40 | let Component; 41 | switch (props.type) { 42 | case 'TouchableOpacity': 43 | Component = TouchableOpacity; 44 | break; 45 | case 'TouchableHighlight': 46 | Component = TouchableHighlight; 47 | break; 48 | case 'TouchableWithoutFeedback': 49 | Component = TouchableWithoutFeedback; 50 | break; 51 | default: 52 | console.warn('TWLoginButton type must be TouchableOpacity or TouchableHighlight or TouchableWithoutFeedback'); 53 | return null; 54 | } 55 | 56 | const onButtonPress = async (e: any): Promise => { 57 | await props.onPress(e); 58 | 59 | try { 60 | const loginURL = await twitter.getLoginUrl(props.callbackUrl); 61 | setAuthURL(loginURL); 62 | } catch (err) { 63 | console.warn(`[getLoginUrl failed] ${err}`); 64 | } 65 | }; 66 | 67 | const onClosePress = (e: any) => { 68 | setVisible(false); 69 | props.onClose(e); 70 | }; 71 | 72 | const onNavigationStateChange = async (webViewState: any) => { 73 | const match = webViewState.url.match(/\?oauth_token=.+&oauth_verifier=(.+)/); 74 | 75 | if (match && match.length > 0) { 76 | setVisible(false); 77 | 78 | /* get access token */ 79 | try { 80 | const response = await twitter.getAccessToken(match[1]); 81 | setToken(response); 82 | } catch (err) { 83 | console.warn(`[getAccessToken failed] ${err}`); 84 | 85 | props.onError(err); 86 | } 87 | } 88 | }; 89 | 90 | useEffect(() => { 91 | if (authURL !== '') { 92 | setVisible(true); 93 | } 94 | }, [authURL]); 95 | 96 | useEffect(() => { 97 | if (!isVisible) { 98 | setAuthURL(''); 99 | } 100 | }, [isVisible]); 101 | 102 | useEffect(() => { 103 | if (token && token.oauth_token && token.oauth_token_secret) { 104 | props.onGetAccessToken(token); 105 | 106 | const options = { 107 | include_entities: false, 108 | skip_status: true, 109 | include_email: true, 110 | }; 111 | 112 | twitter.api('GET', 'account/verify_credentials.json', options).then((response) => { 113 | props.onSuccess(response); 114 | }).catch((err) => { props.onError(err); }); 115 | } 116 | }, [token]); 117 | 118 | return ( 119 | 120 | {props.children} 121 | { }}> 122 | 123 | {props.renderHeader ? props.renderHeader({ onClose: onClosePress }) 124 | :
} 125 | 126 | 127 | 128 | 129 | ); 130 | } 131 | 132 | TWLoginButton.defaultProps = { 133 | type: 'TouchableOpacity', 134 | headerColor: '#f7f7f7', 135 | callbackUrl: null, 136 | closeText: 'close', 137 | onPress: () => { }, 138 | onGetAccessToken: () => { }, 139 | onClose: () => { }, 140 | onError: () => { }, 141 | renderHeader: null, 142 | children: null, 143 | }; 144 | 145 | export default TWLoginButton; 146 | -------------------------------------------------------------------------------- /src/Components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | View, 4 | Text, 5 | TouchableOpacity, 6 | StyleSheet, 7 | Platform, 8 | ViewStyle, 9 | } from 'react-native'; 10 | 11 | const styles = StyleSheet.create({ 12 | container: { 13 | flexDirection: 'row', 14 | justifyContent: 'flex-start', 15 | alignItems: 'center', 16 | height: Platform.OS === 'ios' ? 44 : 56, 17 | borderBottomWidth: StyleSheet.hairlineWidth, 18 | borderBottomColor: '#a7a7aa', 19 | }, 20 | closeButton: { 21 | paddingHorizontal: 16, 22 | }, 23 | }); 24 | 25 | type Props = { 26 | headerColor: string; 27 | textColor: string; 28 | style: ViewStyle; 29 | closeText: string, 30 | onClose: (e: any) => void; 31 | } 32 | 33 | function Header(props: Props) { 34 | return ( 35 | 36 | 37 | {props.closeText} 38 | 39 | 40 | ); 41 | } 42 | 43 | Header.defaultProps = { 44 | headerColor: '#f7f7f7', 45 | textColor: '#333', 46 | style: null, 47 | closeText: 'close', 48 | onClose: () => { }, 49 | }; 50 | 51 | export default Header; 52 | -------------------------------------------------------------------------------- /src/Components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Modal, 4 | SafeAreaView, 5 | } from 'react-native'; 6 | 7 | /* npm */ 8 | import WebView, { WebViewNavigation } from 'react-native-webview'; 9 | 10 | /* components */ 11 | import Header from './Header'; 12 | 13 | type PackageProps = { 14 | visible: boolean, 15 | authURL: string, 16 | onClosePress: () => void, 17 | onWebViewStateChanged: (webViewState: WebViewNavigation) => void, 18 | } 19 | 20 | export type Props = { 21 | headerColor?: string, 22 | textColor?: string, 23 | closeText?: string, 24 | renderHeader?: (props: { onClose: () => void }) => React.ReactElement, 25 | } 26 | 27 | function TWLoginModal(props: Props & PackageProps) { 28 | return ( 29 | { }}> 30 | 31 | {props.renderHeader ? props.renderHeader({ onClose: props.onClosePress }) 32 | :
} 33 | 38 | 39 | 40 | ); 41 | } 42 | 43 | TWLoginModal.defaultProps = { 44 | headerColor: '#f7f7f7', 45 | closeText: 'close', 46 | renderHeader: null, 47 | }; 48 | 49 | export default TWLoginModal; 50 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | /* lib */ 2 | import Request from './request'; 3 | import * as Util from './util'; 4 | import { 5 | Method, 6 | RequestTokenResponse, 7 | AccessTokenResponse, 8 | AccessToken, 9 | } from './types'; 10 | import CustomError from './error'; 11 | 12 | /* const */ 13 | const baseURL: string = 'https://api.twitter.com'; 14 | const apiURL: string = 'https://api.twitter.com/1.1'; 15 | const requestTokenURL: string = '/oauth/request_token'; 16 | const authorizationURL: string = '/oauth/authorize'; 17 | const accessTokenURL: string = '/oauth/access_token'; 18 | 19 | class Client { 20 | ConsumerKey!: string 21 | 22 | ConsumerSecret!: string 23 | 24 | Token!: string 25 | 26 | TokenSecret!: string 27 | 28 | TokenRequestHeaderParams: any = {} 29 | 30 | /** 31 | * set consumer 32 | */ 33 | setConsumerKey = (consumerKey: string, consumerSecret: string): void => { 34 | this.ConsumerKey = consumerKey; 35 | this.ConsumerSecret = consumerSecret; 36 | } 37 | 38 | /** 39 | * set access token 40 | */ 41 | setAccessToken = (token: string, tokenSecret: string): void => { 42 | this.Token = token; 43 | this.TokenSecret = tokenSecret; 44 | } 45 | 46 | /** 47 | * get login redirect url 48 | */ 49 | getLoginUrl = async (callback: string = ''): Promise => { 50 | this.TokenRequestHeaderParams = Util.createTokenRequestHeaderParams(this.ConsumerKey, { callback }); 51 | this.TokenRequestHeaderParams = Util.createSignature(this.TokenRequestHeaderParams, 'POST', baseURL + requestTokenURL, this.ConsumerSecret); 52 | 53 | const result = await Request( 54 | 'POST', 55 | baseURL + requestTokenURL, 56 | this.TokenRequestHeaderParams, 57 | ); 58 | 59 | if ('errors' in result) { 60 | throw new CustomError(result); 61 | } 62 | 63 | this.setAccessToken(result.oauth_token, result.oauth_token_secret); 64 | 65 | return `${baseURL + authorizationURL}?oauth_token=${this.Token}`; 66 | } 67 | 68 | /** 69 | * get access token 70 | */ 71 | getAccessToken = async (verifier: string = ''): Promise => { 72 | this.TokenRequestHeaderParams = Util.createTokenRequestHeaderParams(this.ConsumerKey, { token: this.Token }); 73 | this.TokenRequestHeaderParams = Util.createSignature(this.TokenRequestHeaderParams, 'POST', baseURL + accessTokenURL, this.ConsumerSecret, this.TokenSecret); 74 | this.TokenRequestHeaderParams.oauth_verifier = verifier; 75 | 76 | const result = await Request( 77 | 'POST', 78 | baseURL + accessTokenURL, 79 | this.TokenRequestHeaderParams, 80 | ); 81 | 82 | if ('errors' in result) { 83 | throw new CustomError(result); 84 | } 85 | 86 | this.setAccessToken(result.oauth_token, result.oauth_token_secret); 87 | 88 | return { oauth_token: result.oauth_token, oauth_token_secret: result.oauth_token_secret }; 89 | } 90 | 91 | /** 92 | * call Twitter Api 93 | */ 94 | api = async (method: Method, endpoint: string, params: any = {}): Promise => { 95 | const apiEndpoint = endpoint.slice(0, 1) !== '/' ? `/${endpoint}` : endpoint; 96 | 97 | this.TokenRequestHeaderParams = Util.createTokenRequestHeaderParams(this.ConsumerKey, { token: this.Token, params }); 98 | this.TokenRequestHeaderParams = Util.createSignature(this.TokenRequestHeaderParams, method, apiURL + apiEndpoint, this.ConsumerSecret, this.TokenSecret); 99 | 100 | const result = await Request( 101 | method, 102 | apiURL + (params ? `${apiEndpoint}?${Util.encodeParamsToString(params)}` : apiEndpoint), 103 | this.TokenRequestHeaderParams, 104 | ); 105 | 106 | if ('errors' in result) { 107 | throw new CustomError(result); 108 | } 109 | 110 | return result; 111 | } 112 | 113 | /** 114 | * api("POST",endpoint,params) alias 115 | * will be remove at next version 116 | */ 117 | post = async (endpoint: string, params: any = {}): Promise => { 118 | const result = await this.api('POST', endpoint, params); 119 | 120 | return result; 121 | } 122 | 123 | /** 124 | * api("GET",endpoint,params) alias 125 | * will be remove at next version 126 | */ 127 | get = async (endpoint: string, params: any = {}): Promise => { 128 | const result = await this.api('GET', endpoint, params); 129 | 130 | return result; 131 | } 132 | } 133 | 134 | export default new Client(); 135 | -------------------------------------------------------------------------------- /src/error.ts: -------------------------------------------------------------------------------- 1 | import { ErrorResponse } from './types'; 2 | 3 | export default class CustomError extends Error { 4 | errors: ErrorResponse; 5 | 6 | constructor(obj: ErrorResponse) { 7 | super(JSON.stringify(obj)); 8 | this.name = 'TwitterAPIError'; 9 | this.errors = obj; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useCallback } from 'react'; 2 | import { NativeModules } from 'react-native'; 3 | 4 | /* node_modules */ 5 | import { WebViewNavigation } from 'react-native-webview'; 6 | 7 | /* client */ 8 | import client from './client'; 9 | import { ErrorResponse, AccessToken, TwitterUser } from './types'; 10 | import Modal, { Props as ModalProps } from './Components/Modal'; 11 | 12 | type Props = { 13 | onSuccess: (user: TwitterUser, accessToken: AccessToken) => void, 14 | onError?: (err: ErrorResponse) => void, 15 | } 16 | 17 | const useTwitter = (props?: Props) => { 18 | const [visible, setVisible] = useState(false); 19 | const [authURL, setAuthURL] = useState(''); 20 | const [webViewState, setWebViewState] = useState(null); 21 | const [loggedIn, setLoggedIn] = useState(false); 22 | 23 | const login = async (callback_url?: string) => { 24 | const url: string = await client.getLoginUrl(callback_url); 25 | 26 | setAuthURL(url); 27 | setVisible(true); 28 | }; 29 | 30 | const clearCookies = (callback: (result: boolean) => void = () => { }) => { 31 | NativeModules.Networking.clearCookies(callback); 32 | }; 33 | 34 | const TWModal = useCallback((modalProps: ModalProps) => { 35 | const onWebViewStateChanged = (webViewNavigation: WebViewNavigation) => { 36 | setWebViewState(webViewNavigation); 37 | }; 38 | 39 | return ( 40 | setVisible(false)} 44 | onWebViewStateChanged={onWebViewStateChanged} 45 | headerColor={modalProps.headerColor} 46 | textColor={modalProps.textColor} 47 | closeText={modalProps.closeText} 48 | renderHeader={modalProps.renderHeader} 49 | /> 50 | ); 51 | }, [visible]); 52 | 53 | useEffect(() => { 54 | if (webViewState) { 55 | const match = webViewState.url.match(/\?oauth_token=.+&oauth_verifier=(.+)/); 56 | 57 | if (match && match.length > 0) { 58 | setVisible(false); 59 | setAuthURL(''); 60 | 61 | client.getAccessToken(match[1]).then((response) => { 62 | client.setAccessToken(response.oauth_token, response.oauth_token_secret); 63 | 64 | setLoggedIn(true); 65 | }).catch((err) => { 66 | console.warn(`[getAccessToken failed] ${err}`); 67 | 68 | if (props?.onError) { 69 | props.onError(err); 70 | } 71 | }); 72 | } 73 | } 74 | }, [webViewState]); 75 | 76 | useEffect(() => { 77 | if (loggedIn && props?.onSuccess) { 78 | const options = { 79 | include_entities: false, 80 | skip_status: true, 81 | include_email: true, 82 | }; 83 | 84 | client.api('GET', 'account/verify_credentials.json', options).then((response) => { 85 | props.onSuccess(response, { oauth_token: client.Token, oauth_token_secret: client.TokenSecret }); 86 | 87 | setLoggedIn(false); 88 | }).catch((err) => { 89 | console.warn(`[get("account/verify_credentials.json") failed] ${err}`); 90 | 91 | if (props?.onError) { 92 | props.onError(err); 93 | } 94 | 95 | setLoggedIn(false); 96 | }); 97 | } 98 | }, [loggedIn]); 99 | 100 | return { 101 | twitter: { 102 | login, 103 | clearCookies, 104 | getAccessToken: (): AccessToken => ({ oauth_token: client.Token, oauth_token_secret: client.TokenSecret }), 105 | setAccessToken: client.setAccessToken, 106 | setConsumerKey: client.setConsumerKey, 107 | api: client.api, 108 | post: client.post, 109 | get: client.get, 110 | }, 111 | TWModal, 112 | }; 113 | }; 114 | 115 | export default useTwitter; 116 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* client */ 2 | import Client from './client'; 3 | import TWLoginButton from './Components/Button'; 4 | import { 5 | decodeHTMLEntities, 6 | getRelativeTime, 7 | } from './lib'; 8 | 9 | import useTwitter from './hooks'; 10 | import * as SimpleTwitterTypes from './types'; 11 | 12 | /* export Client as default */ 13 | export default Client; 14 | 15 | /* export other */ 16 | export { 17 | TWLoginButton, 18 | decodeHTMLEntities, 19 | getRelativeTime, 20 | useTwitter, 21 | SimpleTwitterTypes, 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * decode html entity 3 | */ 4 | export const decodeHTMLEntities = (text: string): string => { 5 | const entities: any = { 6 | amp: '&', 7 | apos: '\'', 8 | '#x27': '\'', 9 | '#x2F': '/', 10 | '#39': '\'', 11 | '#47': '/', 12 | lt: '<', 13 | gt: '>', 14 | nbsp: ' ', 15 | quot: '"', 16 | }; 17 | 18 | return text.replace(/&([^;]+);/gm, (match, entity) => entities[entity] || match); 19 | }; 20 | 21 | /** 22 | * get relative time 23 | */ 24 | export const getRelativeTime = (dateTime: Date | string | number): string => { 25 | const created = new Date(dateTime); 26 | const diff = Math.floor((new Date().getTime() - created.getTime()) / 1000); 27 | 28 | if (diff < 60) { 29 | return `${diff}s`; 30 | } 31 | 32 | if (diff < 3600) { 33 | return `${Math.floor(diff / 60)}m`; 34 | } 35 | 36 | if (diff < 86400) { 37 | return `${Math.floor(diff / 3600)}h`; 38 | } 39 | 40 | return `${created.getFullYear()}/${(`00${created.getMonth() + 1}`).slice(-2)}/${(`00${created.getDate()}`).slice(-2)}`; 41 | }; 42 | -------------------------------------------------------------------------------- /src/request.ts: -------------------------------------------------------------------------------- 1 | /* lib */ 2 | import * as Util from './util'; 3 | import { Method, ErrorResponse } from './types'; 4 | 5 | const request = async (method: Method = 'GET', url: string = '', params: any = {}): Promise => { 6 | const uri = url 7 | .replace(/!/g, '%21') 8 | .replace(/'/g, '%27') 9 | .replace(/\(/g, '%28') 10 | .replace(/\)/g, '%29') 11 | .replace(/\*/g, '%2A'); 12 | 13 | const options = { 14 | method, 15 | headers: { 16 | Authorization: Util.createHeaderString(params), 17 | }, 18 | }; 19 | 20 | const response = await fetch(uri, options); 21 | 22 | const contentType = response.headers.get('content-type'); 23 | 24 | /* json */ 25 | if (contentType && contentType.indexOf('application/json') !== -1) { 26 | const result = await response.json(); 27 | 28 | return result; 29 | } 30 | 31 | /* encoded */ 32 | const result = await response.text(); 33 | 34 | return Util.parseFormEncoding(result); 35 | }; 36 | 37 | export default request; 38 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' 2 | 3 | export type ErrorResponse = { 4 | errors: { 5 | code: number, 6 | message: string, 7 | }[] 8 | } 9 | 10 | export type RequestTokenResponse = { 11 | oauth_callback_confirmed: string, 12 | oauth_token: string, 13 | oauth_token_secret: string, 14 | } 15 | 16 | export type AccessTokenResponse = { 17 | oauth_token: string, 18 | oauth_token_secret: string, 19 | screen_name: string, 20 | user_id: string, 21 | } 22 | 23 | export type AccessToken = { 24 | oauth_token: string, 25 | oauth_token_secret: string, 26 | } 27 | 28 | export type TwitterUser = { 29 | created_at: string; 30 | default_profile_image: boolean; 31 | default_profile: boolean; 32 | description?: string | null; 33 | entities: { 34 | description: { 35 | urls?: { 36 | display_url?: string; 37 | expanded_url?: string; 38 | indices?: [number, number] | null; 39 | url: string; 40 | }[] | null 41 | }; 42 | url?: { 43 | urls?: { 44 | display_url?: string; 45 | expanded_url?: string; 46 | indices?: [number, number] | null; 47 | url: string; 48 | }[] | null; 49 | } | null; 50 | }; 51 | favourites_count: number; 52 | followers_count: number; 53 | friends_count: number; 54 | id_str: string; 55 | id: number; 56 | listed_count: number; 57 | location?: string | null; 58 | name: string; 59 | profile_banner_url?: string; 60 | profile_image_url_https: string; 61 | protected: boolean; 62 | screen_name: string; 63 | statuses_count: number; 64 | url?: string | null; 65 | verified: boolean; 66 | withheld_in_countries?: string[]; 67 | withheld_scope?: string; 68 | } 69 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | /* npm */ 2 | import HmacSHA1 from 'crypto-js/hmac-sha1'; 3 | import * as Base64 from 'crypto-js/enc-base64'; 4 | 5 | /** 6 | * random strings (initial length -> 32) 7 | */ 8 | export const randomStrings = (n: number = 32): string => { 9 | const str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 10 | 11 | return Array(...Array(n)).map(() => str.charAt(Math.floor(Math.random() * str.length))).join(''); 12 | }; 13 | 14 | /** 15 | * create header.Authorization string 16 | */ 17 | export const createHeaderString = (params: any): string => `OAuth ${Object.keys(params).sort() 18 | .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) 19 | .join(', ')}`; 20 | 21 | /** 22 | * create string object.join(&) 23 | */ 24 | export const encodeParamsToString = (params: any): string => Object.keys(params).sort() 25 | .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) 26 | .join('&'); 27 | 28 | /** 29 | * if content-type === text/html, parse response.text() 30 | */ 31 | export const parseFormEncoding = (formEncoded: string): any => formEncoded.split('&').reduce((obj, form) => { 32 | const [key, value] = form.split('='); 33 | return { ...obj, [key]: value }; 34 | }, {}); 35 | 36 | /** 37 | * create params 38 | */ 39 | export const createTokenRequestHeaderParams = (consumerKey: string, { callback, token, params }: { callback?: string, token?: string, params?: any }) => ({ 40 | ...(callback ? { oauth_callback: callback } : {}), 41 | oauth_consumer_key: consumerKey, 42 | oauth_nonce: randomStrings(), 43 | oauth_signature_method: 'HMAC-SHA1', 44 | oauth_timestamp: new Date().getTime() / 1000, 45 | ...(token ? { oauth_token: token } : {}), 46 | oauth_version: '1.0', 47 | ...params, 48 | }); 49 | 50 | /** 51 | * create OAuth1.0 signature from params 52 | */ 53 | export const createSignature = (params: object, method: string, url: string, consumerSecret: string, tokenSecret?: string) => { 54 | const encodedParameters = encodeParamsToString(params) 55 | .replace(/!/g, '%21') 56 | .replace(/'/g, '%27') 57 | .replace(/\(/g, '%28') 58 | .replace(/\)/g, '%29') 59 | .replace(/\*/g, '%2A'); 60 | const encodedRequestURL = encodeURIComponent(url); 61 | 62 | const signature = Base64.stringify(HmacSHA1( 63 | `${method}&${encodedRequestURL}&${encodeURIComponent(encodedParameters)}`, 64 | tokenSecret ? `${encodeURIComponent(consumerSecret)}&${encodeURIComponent(tokenSecret)}` : `${encodeURIComponent(consumerSecret)}&`, 65 | )); 66 | 67 | return { ...params, oauth_signature: signature }; 68 | }; 69 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": [ 6 | "esnext", 7 | ], 8 | "jsx": "react-native", 9 | "moduleResolution": "node", 10 | "outDir": "dist", 11 | "declarationDir": "types", 12 | "declaration": true, 13 | "removeComments": true, 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "esModuleInterop": true, 18 | "inlineSourceMap": true, 19 | "noEmitOnError": false, 20 | "inlineSources": true 21 | }, 22 | "include": [ 23 | "src/**/*" 24 | ], 25 | "exclude": [ 26 | "node_modules" 27 | ] 28 | } --------------------------------------------------------------------------------