├── .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 | 
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 | }
--------------------------------------------------------------------------------