├── .eslintrc ├── .gitignore ├── .npmignore ├── .npmrc ├── .nvmrc ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── __test__ ├── __snapshots__ │ ├── google-contacts-dist.test.js.snap │ └── google-contacts.test.js.snap ├── google-contacts-dist.test.js └── google-contacts.test.js ├── demo ├── app.js ├── index.html └── index.js ├── dist ├── google-contacts.js └── google-contacts.js.LICENSE.txt ├── doc └── screenshot.png ├── index.d.ts ├── package.json ├── src ├── button-content.js ├── google-contacts.js ├── icon.js └── index.js └── webpack ├── uglify.json ├── webpack.config.dev.js └── webpack.config.prod.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint-config-ag"], 3 | "rules": { 4 | "react/forbid-prop-types": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_store 3 | .module-cache 4 | npm-debug.log 5 | .c9 6 | yarn.lock 7 | .vscode -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | ._* 3 | .DS_Store 4 | .git 5 | .hg 6 | .lock-wscript 7 | .svn 8 | .wafpickle-* 9 | CVS 10 | npm-debug.log 11 | node_modules/ 12 | .DS_store 13 | .module-cache 14 | npm-debug.log 15 | .c9 -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.0 2 | 3 | - Contact API deprecation solved and People API implementation added 4 | 5 | ## 1.0.1 6 | 7 | - Fix extractGivenNameFromEntry & extractFamilyNameFromEntry is not defined 8 | 9 | ## 1.0.0 10 | 11 | - Update webpack 5. 12 | - Add more field return (https://github.com/kwent/react-google-contacts/pull/2). Thanks @nicks78. 13 | - maxResults props added (https://github.com/kwent/react-google-contacts/pull/8). Thanks @moshfiqrony. 14 | 15 | ## 0.0.1 16 | 17 | - First alpha release. 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Quentin Rousseau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Google Contacts 2 | 3 | > A Google Button to import user's gmail contacts 4 | 5 | React Google Contacts 6 | 7 | ## Install 8 | ``` 9 | npm install react-google-contacts 10 | ``` 11 | 12 | ## How to use 13 | ```js 14 | import React from 'react'; 15 | import ReactDOM from 'react-dom'; 16 | import GoogleContacts from 'react-google-contacts'; 17 | 18 | const responseCallback = (response) => { 19 | console.log(response); 20 | } 21 | 22 | ReactDOM.render( 23 | , 30 | document.getElementById('googleButton') 31 | ); 32 | ``` 33 | 34 | ## Google button without styling or custom button 35 | ```js 36 | ReactDOM.render( 37 | ( 41 | 42 | )} 43 | buttonText="Import" 44 | onSuccess={responseCallback} 45 | onFailure={responseCallback} 46 | />, 47 | document.getElementById('googleButton') 48 | ); 49 | ``` 50 | 51 | ## onSuccess callback 52 | 53 | Callback will return an array of objects (contacts). 54 | 55 | If you use the hostedDomain param, make sure to validate the id_token (a JSON web token) returned by Google on your backend server: 56 | 1. In the `responseGoogle(response) {...}` callback function, you should get back a standard JWT located at `response.hg.id_token` 57 | 2. Send this token to your server (preferably as an `Authorization` header) 58 | 3. Have your server decode the id_token by using a common JWT library such as [jwt-simple](https://github.com/hokaccha/node-jwt-simple) or by sending a GET request to `https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=YOUR_TOKEN_HERE` 59 | 4. The returned decoded token should have an `hd` key equal to the hosted domain you'd like to restrict to. 60 | 61 | 62 | ## Parameters 63 | 64 | | params | value | default value | description | 65 | |:------------:|:--------:|:------------------------------------:|:----------------:| 66 | | clientId | string | REQUIRED | | 67 | | apiKey | string | REQUIRED | | 68 | | maxResults | number | 999 | By passing a number here you can restrict how many results you want to return | 69 | | hostedDomain | string | - | URL of the Javascript file normally hosted by Google | 70 | | accessType | string | online | Can be either 'online' or 'offline'. | 71 | | onFailure | function | REQUIRED | | 72 | | onSuccess | function | REQUIRED | | 73 | | onRequest | function | () => {} | | 74 | | buttonText | string | Import from Gmail | | 75 | | className | string | - | | 76 | | disabledStyle| object | - | | 77 | | loginHint | string | - | | 78 | | prompt | string | consent | | 79 | | tag | string | button | sets element tag (div, a, span, etc | 80 | | type | string | button |sets button type (submit || button) | 81 | | disabled | boolean | false | | 82 | | uxMode | string | popup | The UX mode to use for the sign-in flow. Valid values are popup and redirect. | 83 | | theme | string | light | If set to `dark` the button will follow the Google brand guidelines for dark. Otherwise it will default to light (https://developers.google.com/identity/branding-guidelines) | 84 | | icon | boolean | true | Show (`true`) or hide (`false`) the Google Icon | 85 | | redirectUri | string | - | If using uxMode='redirect', this parameter allows you to override the default redirect_uri that will 86 | | render | function | - | Render prop to use a custom element, use renderProps.onClick | 87 | 88 | ## onSuccess callback ( w/ offline false) 89 | 90 | onSuccess callback returns an array of objects (contacts). 91 | 92 | | property name | value | definition | 93 | |:-------------:|:--------:|:------------------------------------:| 94 | | title | string | First Name and Last Name | 95 | | email | string | Email | 96 | 97 | ## onSuccess callback ( w/ offline true) 98 | 99 | | property name | value | definition | 100 | |:-------------:|:--------:|:------------------------------------:| 101 | | code | object | offline token | 102 | 103 | You can also pass child components such as icons into the button component. 104 | ```js 105 | 111 | 114 | Import from Gmail 115 | 116 | 117 | ``` 118 | 119 | 120 | ## onFailure callback 121 | 122 | onFailure callback is called when either initialization or a signin attempt fails. 123 | 124 | | property name | value | definition | 125 | |:-------------:|:--------:|:------------------------------------:| 126 | | error | string | Error code | 127 | | details | string | Detailed error description | 128 | 129 | 130 | 131 | Common error codes include: 132 | 133 | | error code | description | 134 | |:----------:|:-----------:| 135 | | `idpiframe_initialization_failed` | initialization of the Google Auth API failed (this will occur if a client doesn't have [third party cookies enabled](https://github.com/google/google-api-javascript-client/issues/260)) | 136 | | `popup_closed_by_user` | The user closed the popup before finishing the sign in flow.| 137 | | `access_denied` | The user denied the permission to the scopes required | 138 | | `immediate_failed` | No user could be automatically selected without prompting the consent flow. | 139 | 140 | More details can be found in the official Google docs: 141 | * [GoogleAuth.then(onInit, onError)](https://developers.google.com/identity/sign-in/web/reference#googleauththenoninit-onerror) 142 | * [GoogleAuth.signIn(options)](https://developers.google.com/identity/sign-in/web/reference#googleauthsigninoptions) 143 | * [GoogleAuth.grantOfflineAccess(options)](https://developers.google.com/identity/sign-in/web/reference#googleauthgrantofflineaccessoptions) 144 | 145 | ## Dev Server 146 | ``` 147 | npm run start 148 | ``` 149 | Default dev server runs at localost:8080 in browser. 150 | You can set IP and PORT in webpack.config.dev.js 151 | 152 | ## Run Tests 153 | ``` 154 | npm run test:watch 155 | ``` 156 | 157 | ## Production Bundle 158 | ``` 159 | npm run bundle 160 | ``` 161 | 162 | ## Credits 163 | 164 | Based on the amazing work of @anthonyjgrove: https://github.com/anthonyjgrove/react-google-login 165 | -------------------------------------------------------------------------------- /__test__/__snapshots__/google-contacts-dist.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Google Contacts Dist With children, custom text, and default props has inline styles 1`] = ` 4 | Object { 5 | "alignItems": "center", 6 | "backgroundColor": "#fff", 7 | "border": "1px solid transparent", 8 | "borderRadius": 2, 9 | "boxShadow": "0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)", 10 | "color": "rgba(0, 0, 0, .54)", 11 | "display": "inline-flex", 12 | "fontFamily": "Roboto, sans-serif", 13 | "fontSize": 14, 14 | "fontWeight": "500", 15 | "padding": 0, 16 | } 17 | `; 18 | 19 | exports[`Google Contacts Dist With children, custom text, and default props render the button 1`] = ` 20 | 100 | `; 101 | 102 | exports[`Google Contacts Dist With custom class and custom style has inline styles 1`] = ` 103 | Object { 104 | "alignItems": "center", 105 | "backgroundColor": "#fff", 106 | "border": "1px solid transparent", 107 | "borderRadius": 2, 108 | "boxShadow": "0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)", 109 | "color": "rgba(0, 0, 0, .54)", 110 | "display": "inline-flex", 111 | "fontFamily": "Roboto, sans-serif", 112 | "fontSize": 14, 113 | "fontWeight": "500", 114 | "padding": 0, 115 | } 116 | `; 117 | 118 | exports[`Google Contacts Dist With custom class and custom style render the button 1`] = ` 119 | 200 | `; 201 | 202 | exports[`Google Contacts Dist With custom class and default props has inline styles 1`] = ` 203 | Object { 204 | "alignItems": "center", 205 | "backgroundColor": "#fff", 206 | "border": "1px solid transparent", 207 | "borderRadius": 2, 208 | "boxShadow": "0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)", 209 | "color": "rgba(0, 0, 0, .54)", 210 | "display": "inline-flex", 211 | "fontFamily": "Roboto, sans-serif", 212 | "fontSize": 14, 213 | "fontWeight": "500", 214 | "padding": 0, 215 | } 216 | `; 217 | 218 | exports[`Google Contacts Dist With custom class and default props render the button 1`] = ` 219 | 300 | `; 301 | 302 | exports[`Google Contacts Dist With custom text and default props has inline styles 1`] = ` 303 | Object { 304 | "alignItems": "center", 305 | "backgroundColor": "#fff", 306 | "border": "1px solid transparent", 307 | "borderRadius": 2, 308 | "boxShadow": "0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)", 309 | "color": "rgba(0, 0, 0, .54)", 310 | "display": "inline-flex", 311 | "fontFamily": "Roboto, sans-serif", 312 | "fontSize": 14, 313 | "fontWeight": "500", 314 | "padding": 0, 315 | } 316 | `; 317 | 318 | exports[`Google Contacts Dist With custom text and default props render the button 1`] = ` 319 | 399 | `; 400 | 401 | exports[`Google Contacts Dist With default props has inline styles 1`] = ` 402 | Object { 403 | "alignItems": "center", 404 | "backgroundColor": "#fff", 405 | "border": "1px solid transparent", 406 | "borderRadius": 2, 407 | "boxShadow": "0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)", 408 | "color": "rgba(0, 0, 0, .54)", 409 | "display": "inline-flex", 410 | "fontFamily": "Roboto, sans-serif", 411 | "fontSize": 14, 412 | "fontWeight": "500", 413 | "padding": 0, 414 | } 415 | `; 416 | 417 | exports[`Google Contacts Dist With default props render the button 1`] = ` 418 | 498 | `; 499 | 500 | exports[`Google Contacts Dist With handles custom tag prop has inline styles 1`] = ` 501 | Object { 502 | "alignItems": "center", 503 | "backgroundColor": "#fff", 504 | "border": "1px solid transparent", 505 | "borderRadius": 2, 506 | "boxShadow": "0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)", 507 | "color": "rgba(0, 0, 0, .54)", 508 | "display": "inline-flex", 509 | "fontFamily": "Roboto, sans-serif", 510 | "fontSize": 14, 511 | "fontWeight": "500", 512 | "padding": 0, 513 | } 514 | `; 515 | 516 | exports[`Google Contacts Dist With handles custom tag prop render the button 1`] = ` 517 |
541 |
551 | 556 | 560 | 564 | 568 | 572 | 576 | 580 | 581 | 582 |
583 | 594 | Import from Gmail 595 | 596 |
597 | `; 598 | -------------------------------------------------------------------------------- /__test__/__snapshots__/google-contacts.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Google Contacts With children, custom text, and default props has inline styles 1`] = ` 4 | Object { 5 | "alignItems": "center", 6 | "backgroundColor": "#fff", 7 | "border": "1px solid transparent", 8 | "borderRadius": 2, 9 | "boxShadow": "0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)", 10 | "color": "rgba(0, 0, 0, .54)", 11 | "display": "inline-flex", 12 | "fontFamily": "Roboto, sans-serif", 13 | "fontSize": 14, 14 | "fontWeight": "500", 15 | "padding": 0, 16 | } 17 | `; 18 | 19 | exports[`Google Contacts With children, custom text, and default props render the button 1`] = ` 20 | 100 | `; 101 | 102 | exports[`Google Contacts With custom class and custom style has inline styles 1`] = ` 103 | Object { 104 | "alignItems": "center", 105 | "backgroundColor": "#fff", 106 | "border": "1px solid transparent", 107 | "borderRadius": 2, 108 | "boxShadow": "0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)", 109 | "color": "rgba(0, 0, 0, .54)", 110 | "display": "inline-flex", 111 | "fontFamily": "Roboto, sans-serif", 112 | "fontSize": 14, 113 | "fontWeight": "500", 114 | "padding": 0, 115 | } 116 | `; 117 | 118 | exports[`Google Contacts With custom class and custom style render the button 1`] = ` 119 | 200 | `; 201 | 202 | exports[`Google Contacts With custom class and default props has inline styles 1`] = ` 203 | Object { 204 | "alignItems": "center", 205 | "backgroundColor": "#fff", 206 | "border": "1px solid transparent", 207 | "borderRadius": 2, 208 | "boxShadow": "0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)", 209 | "color": "rgba(0, 0, 0, .54)", 210 | "display": "inline-flex", 211 | "fontFamily": "Roboto, sans-serif", 212 | "fontSize": 14, 213 | "fontWeight": "500", 214 | "padding": 0, 215 | } 216 | `; 217 | 218 | exports[`Google Contacts With custom class and default props render the button 1`] = ` 219 | 300 | `; 301 | 302 | exports[`Google Contacts With custom text and default props has inline styles 1`] = ` 303 | Object { 304 | "alignItems": "center", 305 | "backgroundColor": "#fff", 306 | "border": "1px solid transparent", 307 | "borderRadius": 2, 308 | "boxShadow": "0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)", 309 | "color": "rgba(0, 0, 0, .54)", 310 | "display": "inline-flex", 311 | "fontFamily": "Roboto, sans-serif", 312 | "fontSize": 14, 313 | "fontWeight": "500", 314 | "padding": 0, 315 | } 316 | `; 317 | 318 | exports[`Google Contacts With custom text and default props render the button 1`] = ` 319 | 399 | `; 400 | 401 | exports[`Google Contacts With default props has inline styles 1`] = ` 402 | Object { 403 | "alignItems": "center", 404 | "backgroundColor": "#fff", 405 | "border": "1px solid transparent", 406 | "borderRadius": 2, 407 | "boxShadow": "0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)", 408 | "color": "rgba(0, 0, 0, .54)", 409 | "display": "inline-flex", 410 | "fontFamily": "Roboto, sans-serif", 411 | "fontSize": 14, 412 | "fontWeight": "500", 413 | "padding": 0, 414 | } 415 | `; 416 | 417 | exports[`Google Contacts With default props render the button 1`] = ` 418 | 498 | `; 499 | 500 | exports[`Google Contacts With handles custom tag prop has inline styles 1`] = ` 501 | Object { 502 | "alignItems": "center", 503 | "backgroundColor": "#fff", 504 | "border": "1px solid transparent", 505 | "borderRadius": 2, 506 | "boxShadow": "0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)", 507 | "color": "rgba(0, 0, 0, .54)", 508 | "display": "inline-flex", 509 | "fontFamily": "Roboto, sans-serif", 510 | "fontSize": 14, 511 | "fontWeight": "500", 512 | "padding": 0, 513 | } 514 | `; 515 | 516 | exports[`Google Contacts With handles custom tag prop render the button 1`] = ` 517 |
541 |
551 | 556 | 560 | 564 | 568 | 572 | 576 | 580 | 581 | 582 |
583 | 594 | Import from Gmail 595 | 596 |
597 | `; 598 | -------------------------------------------------------------------------------- /__test__/google-contacts-dist.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { create } from 'react-test-renderer' 3 | import { configure, shallow } from 'enzyme' 4 | import Adapter from 'enzyme-adapter-react-16' 5 | 6 | import GoogleContactsDist from '../dist/google-contacts' 7 | 8 | configure({ adapter: new Adapter() }) 9 | 10 | describe('Google Contacts Dist', () => { 11 | describe('With default props', () => { 12 | const props = { 13 | onSuccess() {}, 14 | onFailure() {}, 15 | clientId: 'YOUR_CLIENT_ID', 16 | apiKey: 'YOUR_API_KEY', 17 | } 18 | 19 | test('render the button', () => { 20 | const component = create() 21 | const tree = component.toJSON() 22 | expect(tree).toMatchSnapshot() 23 | }) 24 | 25 | const button = shallow() 26 | 27 | test('does not have a class attr', () => { 28 | expect(button.prop('className')).toEqual(undefined) 29 | }) 30 | 31 | test('has inline styles', () => { 32 | expect(button.prop('style')).toMatchSnapshot() 33 | }) 34 | 35 | test('displays a button element when tag prop is not set', () => { 36 | expect(button.type()).toEqual('button') 37 | }) 38 | }) 39 | describe('With custom text and default props', () => { 40 | const buttonText = 'buttonText' 41 | 42 | const props = { 43 | onSuccess() {}, 44 | onFailure() {}, 45 | clientId: 'YOUR_CLIENT_ID', 46 | apiKey: 'YOUR_API_KEY', 47 | buttonText 48 | } 49 | 50 | test('render the button', () => { 51 | const component = create() 52 | const tree = component.toJSON() 53 | expect(tree).toMatchSnapshot() 54 | }) 55 | 56 | const button = shallow() 57 | 58 | test('does not have a class attr', () => { 59 | expect(button.prop('className')).toEqual(undefined) 60 | }) 61 | 62 | test('has inline styles', () => { 63 | expect(button.prop('style')).toMatchSnapshot() 64 | }) 65 | 66 | test('displays a button element when tag prop is not set', () => { 67 | expect(button.type()).toEqual('button') 68 | }) 69 | }) 70 | describe('With custom class and default props', () => { 71 | const className = 'test-class' 72 | 73 | const props = { 74 | onSuccess() {}, 75 | onFailure() {}, 76 | clientId: 'YOUR_CLIENT_ID', 77 | apiKey: 'YOUR_API_KEY', 78 | className 79 | } 80 | 81 | test('render the button', () => { 82 | const component = create() 83 | const tree = component.toJSON() 84 | expect(tree).toMatchSnapshot() 85 | }) 86 | 87 | const button = shallow() 88 | 89 | test('does not have a class attr', () => { 90 | expect(button.prop('className')).toEqual(className) 91 | }) 92 | 93 | test('has inline styles', () => { 94 | expect(button.prop('style')).toMatchSnapshot() 95 | }) 96 | 97 | test('displays a button element when tag prop is not set', () => { 98 | expect(button.type()).toEqual('button') 99 | }) 100 | }) 101 | describe('With custom class and custom style', () => { 102 | const className = 'test-class' 103 | const style = { color: 'red' } 104 | const props = { 105 | onSuccess() {}, 106 | onFailure() {}, 107 | clientId: 'YOUR_CLIENT_ID', 108 | apiKey: 'YOUR_API_KEY', 109 | className, 110 | style 111 | } 112 | 113 | test('render the button', () => { 114 | const component = create() 115 | const tree = component.toJSON() 116 | expect(tree).toMatchSnapshot() 117 | }) 118 | 119 | const button = shallow() 120 | 121 | test('does not have a class attr', () => { 122 | expect(button.prop('className')).toEqual(className) 123 | }) 124 | 125 | test('has inline styles', () => { 126 | expect(button.prop('style')).toMatchSnapshot() 127 | }) 128 | 129 | test('displays a button element when tag prop is not set', () => { 130 | expect(button.type()).toEqual('button') 131 | }) 132 | }) 133 | describe('With children, custom text, and default props', () => { 134 | const children = 'test' 135 | const buttonText = 'buttonText' 136 | const props = { 137 | onSuccess() {}, 138 | onFailure() {}, 139 | clientId: 'YOUR_CLIENT_ID', 140 | apiKey: 'YOUR_API_KEY', 141 | buttonText 142 | } 143 | 144 | test('render the button', () => { 145 | const component = create({children}) 146 | const tree = component.toJSON() 147 | expect(tree).toMatchSnapshot() 148 | }) 149 | 150 | const button = shallow({children}) 151 | 152 | test('does not have a class attr', () => { 153 | expect(button.prop('className')).toEqual(undefined) 154 | }) 155 | 156 | test('has inline styles', () => { 157 | expect(button.prop('style')).toMatchSnapshot() 158 | }) 159 | 160 | test('displays a button element when tag prop is not set', () => { 161 | expect(button.type()).toEqual('button') 162 | }) 163 | }) 164 | describe('With handles custom tag prop', () => { 165 | const tag = 'div' 166 | 167 | const props = { 168 | onSuccess() {}, 169 | onFailure() {}, 170 | clientId: 'YOUR_CLIENT_ID', 171 | apiKey: 'YOUR_API_KEY', 172 | tag 173 | } 174 | 175 | test('render the button', () => { 176 | const component = create() 177 | const tree = component.toJSON() 178 | expect(tree).toMatchSnapshot() 179 | }) 180 | 181 | const button = shallow() 182 | 183 | test('does not have a class attr', () => { 184 | expect(button.prop('className')).toEqual(undefined) 185 | }) 186 | 187 | test('has inline styles', () => { 188 | expect(button.prop('style')).toMatchSnapshot() 189 | }) 190 | 191 | test('displays a button element when tag prop is not set', () => { 192 | expect(button.type()).toEqual(tag) 193 | }) 194 | }) 195 | }) 196 | -------------------------------------------------------------------------------- /__test__/google-contacts.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { create } from 'react-test-renderer' 3 | import { configure, shallow } from 'enzyme' 4 | import Adapter from 'enzyme-adapter-react-16' 5 | 6 | import GoogleContacts from '../src' 7 | 8 | configure({ adapter: new Adapter() }) 9 | 10 | describe('Google Contacts', () => { 11 | describe('With default props', () => { 12 | const props = { 13 | onSuccess() {}, 14 | onFailure() {}, 15 | clientId: 'YOUR_CLIENT_ID', 16 | apiKey: 'YOUR_API_KEY', 17 | } 18 | 19 | test('render the button', () => { 20 | const component = create() 21 | const tree = component.toJSON() 22 | expect(tree).toMatchSnapshot() 23 | }) 24 | 25 | const button = shallow() 26 | 27 | test('does not have a class attr', () => { 28 | expect(button.prop('className')).toEqual(undefined) 29 | }) 30 | 31 | test('has inline styles', () => { 32 | expect(button.prop('style')).toMatchSnapshot() 33 | }) 34 | 35 | test('displays a button element when tag prop is not set', () => { 36 | expect(button.type()).toEqual('button') 37 | }) 38 | }) 39 | describe('With custom text and default props', () => { 40 | const buttonText = 'buttonText' 41 | 42 | const props = { 43 | onSuccess() {}, 44 | onFailure() {}, 45 | clientId: 'YOUR_CLIENT_ID', 46 | apiKey: 'YOUR_API_KEY', 47 | buttonText 48 | } 49 | 50 | test('render the button', () => { 51 | const component = create() 52 | const tree = component.toJSON() 53 | expect(tree).toMatchSnapshot() 54 | }) 55 | 56 | const button = shallow() 57 | 58 | test('does not have a class attr', () => { 59 | expect(button.prop('className')).toEqual(undefined) 60 | }) 61 | 62 | test('has inline styles', () => { 63 | expect(button.prop('style')).toMatchSnapshot() 64 | }) 65 | 66 | test('displays a button element when tag prop is not set', () => { 67 | expect(button.type()).toEqual('button') 68 | }) 69 | }) 70 | describe('With custom class and default props', () => { 71 | const className = 'test-class' 72 | 73 | const props = { 74 | onSuccess() {}, 75 | onFailure() {}, 76 | clientId: 'YOUR_CLIENT_ID', 77 | apiKey: 'YOUR_API_KEY', 78 | className 79 | } 80 | 81 | test('render the button', () => { 82 | const component = create() 83 | const tree = component.toJSON() 84 | expect(tree).toMatchSnapshot() 85 | }) 86 | 87 | const button = shallow() 88 | 89 | test('does not have a class attr', () => { 90 | expect(button.prop('className')).toEqual(className) 91 | }) 92 | 93 | test('has inline styles', () => { 94 | expect(button.prop('style')).toMatchSnapshot() 95 | }) 96 | 97 | test('displays a button element when tag prop is not set', () => { 98 | expect(button.type()).toEqual('button') 99 | }) 100 | }) 101 | describe('With custom class and custom style', () => { 102 | const className = 'test-class' 103 | const style = { color: 'red' } 104 | const props = { 105 | onSuccess() {}, 106 | onFailure() {}, 107 | clientId: 'YOUR_CLIENT_ID', 108 | apiKey: 'YOUR_API_KEY', 109 | className, 110 | style 111 | } 112 | 113 | test('render the button', () => { 114 | const component = create() 115 | const tree = component.toJSON() 116 | expect(tree).toMatchSnapshot() 117 | }) 118 | 119 | const button = shallow() 120 | 121 | test('does not have a class attr', () => { 122 | expect(button.prop('className')).toEqual(className) 123 | }) 124 | 125 | test('has inline styles', () => { 126 | expect(button.prop('style')).toMatchSnapshot() 127 | }) 128 | 129 | test('displays a button element when tag prop is not set', () => { 130 | expect(button.type()).toEqual('button') 131 | }) 132 | }) 133 | describe('With children, custom text, and default props', () => { 134 | const children = 'test' 135 | const buttonText = 'buttonText' 136 | const props = { 137 | onSuccess() {}, 138 | onFailure() {}, 139 | clientId: 'YOUR_CLIENT_ID', 140 | apiKey: 'YOUR_API_KEY', 141 | buttonText 142 | } 143 | 144 | test('render the button', () => { 145 | const component = create({children}) 146 | const tree = component.toJSON() 147 | expect(tree).toMatchSnapshot() 148 | }) 149 | 150 | const button = shallow({children}) 151 | 152 | test('does not have a class attr', () => { 153 | expect(button.prop('className')).toEqual(undefined) 154 | }) 155 | 156 | test('has inline styles', () => { 157 | expect(button.prop('style')).toMatchSnapshot() 158 | }) 159 | 160 | test('displays a button element when tag prop is not set', () => { 161 | expect(button.type()).toEqual('button') 162 | }) 163 | }) 164 | describe('With handles custom tag prop', () => { 165 | const tag = 'div' 166 | 167 | const props = { 168 | onSuccess() {}, 169 | onFailure() {}, 170 | clientId: 'YOUR_CLIENT_ID', 171 | apiKey: 'YOUR_API_KEY', 172 | tag 173 | } 174 | 175 | test('render the button', () => { 176 | const component = create() 177 | const tree = component.toJSON() 178 | expect(tree).toMatchSnapshot() 179 | }) 180 | 181 | const button = shallow() 182 | 183 | test('does not have a class attr', () => { 184 | expect(button.prop('className')).toEqual(undefined) 185 | }) 186 | 187 | test('has inline styles', () => { 188 | expect(button.prop('style')).toMatchSnapshot() 189 | }) 190 | 191 | test('displays a button element when tag prop is not set', () => { 192 | expect(button.type()).toEqual(tag) 193 | }) 194 | }) 195 | }) 196 | -------------------------------------------------------------------------------- /demo/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import GoogleContacts from '../src/index' 3 | 4 | const clientId = '' // 5 | const apiKey = '' // 6 | 7 | const success = response => { 8 | console.log(response) // eslint-disable-line 9 | } 10 | 11 | const error = response => { 12 | console.error(response) // eslint-disable-line 13 | } 14 | 15 | const loading = () => { 16 | console.log('loading') // eslint-disable-line 17 | } 18 | 19 | const App = () => ( 20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | ( 37 | 40 | )} 41 | theme="dark" 42 | /> 43 |
44 | ) 45 | 46 | export default App 47 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import App from './app' 4 | 5 | const renderApp = Component => { 6 | const app = document.getElementById('google-contacts') 7 | 8 | render(, app) 9 | } 10 | 11 | renderApp(App) 12 | 13 | if (module.hot) { 14 | module.hot.accept('./app.js', () => { 15 | /* eslint-disable global-require */ 16 | const app = require('./app').default 17 | renderApp(app) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /dist/google-contacts.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define(["react"],t):"object"==typeof exports?exports.GoogleContacts=t(require("react")):e.GoogleContacts=t(e.react)}("undefined"!=typeof self?self:this,(function(e){return(()=>{"use strict";var t={437:(e,t,n)=>{n.r(t),n.d(t,{default:()=>O});var o=n(297),r=n.n(o);function i(e){var t=e.active;return r().createElement("div",{style:{background:t?"#eee":"#fff",borderRadius:2,marginRight:10,padding:10}},r().createElement("svg",{height:"18",width:"18",xmlns:"http://www.w3.org/2000/svg"},r().createElement("g",{fill:"#000",fillRule:"evenodd"},r().createElement("path",{d:"M9 3.48c1.69 0 2.83.73 3.48 1.34l2.54-2.48C13.46.89 11.43 0 9 0 5.48 0 2.44 2.02.96 4.96l2.91 2.26C4.6 5.05 6.62 3.48 9 3.48z",fill:"#EA4335"}),r().createElement("path",{d:"M17.64 9.2c0-.74-.06-1.28-.19-1.84H9v3.34h4.96c-.1.83-.64 2.08-1.84 2.92l2.84 2.2c1.7-1.57 2.68-3.88 2.68-6.62z",fill:"#4285F4"}),r().createElement("path",{d:"M3.88 10.78A5.54 5.54 0 0 1 3.58 9c0-.62.11-1.22.29-1.78L.96 4.96A9.008 9.008 0 0 0 0 9c0 1.45.35 2.82.96 4.04l2.92-2.26z",fill:"#FBBC05"}),r().createElement("path",{d:"M9 18c2.43 0 4.47-.8 5.96-2.18l-2.84-2.2c-.76.53-1.78.9-3.12.9-2.38 0-4.4-1.57-5.12-3.74L.97 13.04C2.45 15.98 5.48 18 9 18z",fill:"#34A853"}),r().createElement("path",{d:"M0 0h18v18H0z",fill:"none"}))))}const a=function(e){var t=e.children,n=e.icon;return r().createElement("span",{style:{paddingRight:10,fontWeight:500,paddingLeft:n?0:10,paddingTop:10,paddingBottom:10}},t)};function c(e){return c="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},c(e)}function l(e){return function(e){if(Array.isArray(e))return s(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return s(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return s(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,o=new Array(t);n1&&void 0!==arguments[1]?arguments[1]:null,o=this.props,r=o.onFailure,i=o.maxResults;e&&window.gapi.client.request({path:"https://people.googleapis.com/v1/people/me/connections",params:p({personFields:"names,emailAddresses",pageSize:i>1e3?1e3:i},n&&{pageToken:n})}).then((function(n){return t.handleNextDataFetch(n,e)}),(function(e){return r(e)}))}},{key:"handleNextDataFetch",value:function(e,t){var n=this.props.maxResults,o=JSON.parse(e.body);this.allData=[].concat(l(this.allData),l(o.connections)),"nextPageToken"in o&&n>this.allData.length?this.handleImportContacts(t,o.nextPageToken):this.handleParseContacts()}},{key:"handleParseContacts",value:function(){var e=this.props,t=e.onSuccess,n=e.onFailure,o=[];try{for(var r=0;r0&&o.push({email:i.emailAddresses[0].value,title:"names"in i?i.names[0].displayName:i.emailAddresses[0].value})}t(o)}catch(e){n("Error to fetch contacts")}}},{key:"signIn",value:function(e){var t=this;this.allData=[];var n=this.state.disable,o=this.props,r=o.prompt,i=o.onRequest,a=o.accessType,c=o.onSuccess;i(),e&&e.preventDefault(),n||("online"===a&&(this.client.callback=function(e){t.handleImportContacts(e)},null===window.gapi.client.getToken()?this.client.requestAccessToken({prompt:r}):this.client.requestAccessToken({prompt:""})),"offline"===a&&(this.client.callback=c,null===window.gapi.client.getToken()?this.client.requestCode({prompt:r}):this.client.requestCode({prompt:""})))}},{key:"render",value:function(){var e=this,t=this.props,n=t.tag,o=t.type,c=t.className,l=t.disabledStyle,s=t.buttonText,u=t.children,p=t.render,d=t.theme,f=t.icon,h=t.disabled,y=this.state,m=y.active,b=y.hovered,g=y.disabled||h;if(p)return p({onClick:this.signIn});var v={backgroundColor:"dark"===d?"rgb(66, 133, 244)":"#fff",display:"inline-flex",alignItems:"center",color:"dark"===d?"#fff":"rgba(0, 0, 0, .54)",boxShadow:"0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)",padding:0,borderRadius:2,border:"1px solid transparent",fontSize:14,fontWeight:"500",fontFamily:"Roboto, sans-serif"},w={cursor:"pointer",opacity:.9},O={cursor:"pointer",backgroundColor:"dark"===d?"#3367D6":"#eee",color:"dark"===d?"#fff":"rgba(0, 0, 0, .54)",opacity:1},j=g?Object.assign({},v,l):m?Object.assign({},v,O):b?Object.assign({},v,w):v;return r().createElement(n,{onMouseEnter:function(){return e.setState({hovered:!0})},onMouseLeave:function(){return e.setState({hovered:!1,active:!1})},onMouseDown:function(){return e.setState({active:!0})},onMouseUp:function(){return e.setState({active:!1})},onClick:this.signIn,style:j,type:o,disabled:g,className:c},[f&&r().createElement(i,{key:1,active:m}),r().createElement(a,{key:2,icon:f},u||s)])}}],n&&f(t.prototype,n),o&&f(t,o),Object.defineProperty(t,"prototype",{writable:!1}),s}(o.Component);w.defaultProps={accessType:"online",buttonText:"Import from Gmail",disabled:!1,disabledStyle:{opacity:.6},icon:!0,maxResults:999,onRequest:function(){},prompt:"consent",tag:"button",theme:"light",type:"button",uxMode:"popup"};const O=w},297:t=>{t.exports=e}},n={};function o(e){if(n[e])return n[e].exports;var r=n[e]={exports:{}};return t[e](r,r.exports,o),r.exports}return o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var n in t)o.o(t,n)&&!o.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o(437)})()})); -------------------------------------------------------------------------------- /dist/google-contacts.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * The buffer module from node.js, for the browser. 3 | * 4 | * @author Feross Aboukhadijeh 5 | * @license MIT 6 | */ 7 | 8 | /*! http://mths.be/fromcodepoint v0.1.0 by @mathias */ 9 | 10 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 11 | -------------------------------------------------------------------------------- /doc/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwent/react-google-contacts/03ae01b891603d22d805a217a272b4687e549c95/doc/screenshot.png -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for react-google-contacts v0.0.1 2 | // Project: https://github.com/kwent/react-google-contacts 3 | // Definitions by: Ruslan Ibragimov 4 | import {Component, ReactNode, CSSProperties} from 'react'; 5 | 6 | export as namespace ReactGoogleContacts; 7 | 8 | interface AuthResponse { 9 | readonly access_token: string; 10 | readonly id_token: string; 11 | readonly login_hint: string; 12 | readonly expires_in: number; 13 | readonly first_issued_at: number; 14 | readonly expires_at: number; 15 | } 16 | 17 | interface BasicProfile { 18 | getId(): string; 19 | getEmail(): string; 20 | getName(): string; 21 | getGivenName(): string; 22 | getFamilyName(): string; 23 | getImageUrl(): string; 24 | } 25 | 26 | // Based on https://developers.google.com/identity/sign-in/web/reference 27 | export interface GoogleContactsResponse { 28 | getBasicProfile(): BasicProfile; 29 | getAuthResponse(): AuthResponse; 30 | getGrantedScopes(): string; 31 | getHostedDomain(): string; 32 | getId(): string; 33 | hasGrantedScopes(scopes: string): boolean; 34 | disconnect(): void; 35 | grantOfflineAccess(options: GrantOfflineAccessOptions): Promise; 36 | signIn(options: SignInOptions): Promise; 37 | grant(options: SignInOptions): Promise; 38 | } 39 | 40 | interface GrantOfflineAccessOptions { 41 | readonly redirect_uri?: string; 42 | } 43 | 44 | interface SignInOptions { 45 | readonly app_package_name?: string; 46 | readonly fetch_basic_profile?: boolean; 47 | readonly prompt?: string; 48 | } 49 | 50 | export interface GoogleContactsResponseOffline { 51 | readonly code: string; 52 | } 53 | 54 | export interface GoogleContactsProps { 55 | readonly onSuccess: (response: GoogleContactsResponse | GoogleContactsResponseOffline) => void, 56 | readonly onFailure: (error: any) => void, 57 | readonly clientId: string, 58 | readonly jsSrc?: string, 59 | readonly onRequest?: () => void, 60 | readonly buttonText?: string, 61 | readonly className?: string, 62 | readonly redirectUri?: string, 63 | readonly cookiePolicy?: string, 64 | readonly loginHint?: string, 65 | readonly hostedDomain?: string, 66 | readonly prompt?: string, 67 | readonly responseType?: string, 68 | readonly children?: ReactNode, 69 | readonly style?: CSSProperties, 70 | readonly tag?: string, 71 | readonly disabled?: boolean; 72 | readonly uxMode?: string; 73 | readonly disabledStyle?: CSSProperties; 74 | readonly type?: string; 75 | readonly accessType?: string; 76 | readonly maxResults?: number; 77 | readonly render?: (props?: { onClick: () => void }) => JSX.Element; 78 | } 79 | 80 | export class GoogleContacts extends Component { 81 | public signIn(e?: Event): void; 82 | } 83 | 84 | export default GoogleContacts; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-google-contacts", 3 | "version": "2.0.0", 4 | "description": "A Google Button to import user's gmail contacts", 5 | "main": "dist/google-contacts.js", 6 | "scripts": { 7 | "start": "webpack serve --config webpack/webpack.config.dev.js", 8 | "dev": "npm run start", 9 | "bundle": "webpack --config webpack/webpack.config.prod.js; git add ./dist/google-contacts.js", 10 | "test": "jest", 11 | "test:watch": "jest --watch", 12 | "lint": "eslint ./src", 13 | "lint:fix": "eslint ./src --fix", 14 | "clean": "rm -rf node_modules dist; rm yarn.lock" 15 | }, 16 | "pre-commit": [ 17 | "test", 18 | "lint", 19 | "bundle" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/kwent/react-google-contacts.git" 24 | }, 25 | "keywords": [ 26 | "react", 27 | "reactjs", 28 | "react-component", 29 | "google-contacts", 30 | "google-oAuth2", 31 | "google-oAuth" 32 | ], 33 | "author": { 34 | "name": "Quentin Rousseau", 35 | "email": "contact@quent.in" 36 | }, 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/kwent/react-google-contacts/issues" 40 | }, 41 | "homepage": "https://github.com/kwent/react-google-contacts", 42 | "dependencies": { 43 | "@types/react": "*", 44 | "prop-types": "^15.6.0" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "7.12.10", 48 | "@babel/plugin-proposal-object-rest-spread": "7.12.1", 49 | "@babel/plugin-transform-runtime": "7.12.10", 50 | "@babel/preset-env": "7.12.11", 51 | "@babel/preset-react": "7.12.10", 52 | "autoprefixer": "10.2.3", 53 | "babel-core": "7.0.0-bridge.0", 54 | "babel-jest": "26.6.3", 55 | "babel-loader": "8.2.2", 56 | "babel-plugin-transform-react-constant-elements": "6.23.0", 57 | "babel-plugin-transform-react-inline-elements": "6.22.0", 58 | "babel-plugin-transform-react-remove-prop-types": "0.4.24", 59 | "enzyme": "3.11.0", 60 | "enzyme-adapter-react-16": "1.15.6", 61 | "eslint-config-ag": "3.1.0", 62 | "jest": "26.6.3", 63 | "pre-commit": "^1.2.2", 64 | "react": "^17.0.1", 65 | "react-dom": "^17.0.1", 66 | "react-hot-loader": "4.13.0", 67 | "react-test-renderer": "17.0.1", 68 | "webpack": "5.17.0", 69 | "webpack-cli": "4.10.0", 70 | "webpack-dev-server": "3.11.2" 71 | }, 72 | "peerDependencies": { 73 | "react": "^16.0.0", 74 | "react-dom": "^16.0.0" 75 | }, 76 | "types": "./index.d.ts", 77 | "babel": { 78 | "presets": [ 79 | "@babel/preset-env", 80 | "@babel/preset-react" 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/button-content.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ButtonContent = ({ children, icon }) => ( 4 | {children} 5 | ) 6 | 7 | export default ButtonContent 8 | -------------------------------------------------------------------------------- /src/google-contacts.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | /* eslint-disable better-mutation/no-mutating-methods */ 3 | 4 | import React, { Component } from 'react' 5 | import PropTypes from 'prop-types' 6 | 7 | import Icon from './icon' 8 | import ButtonContent from './button-content' 9 | 10 | const SCOPE = 'https://www.googleapis.com/auth/contacts.readonly' 11 | 12 | class GoogleContacts extends Component { 13 | constructor(props) { 14 | super(props) 15 | this.signIn = this.signIn.bind(this) 16 | this.handleImportContacts = this.handleImportContacts.bind(this) 17 | this.handleParseContacts = this.handleParseContacts.bind(this) 18 | this.loadApi = this.loadApi.bind(this) 19 | this.loadClient = this.loadClient.bind(this) 20 | this.state = { 21 | hovered: false, 22 | active: false 23 | } 24 | this.allData = [] 25 | this.client = null 26 | } 27 | 28 | componentDidMount() { 29 | const element = document.getElementsByTagName('script')[0] 30 | const firstJs = element 31 | 32 | if (!document.getElementById('google-contacts-api')) { 33 | const js = document.createElement('script') 34 | js.id = 'google-contacts-api' 35 | js.src = 'https://apis.google.com/js/api.js' 36 | if (firstJs && firstJs.parentNode) { 37 | firstJs.parentNode.insertBefore(js, firstJs) 38 | } else { 39 | document.head.appendChild(js) 40 | } 41 | js.onload = this.loadApi 42 | } 43 | 44 | if (!document.getElementById('google-contacts-gsi')) { 45 | const js = document.createElement('script') 46 | js.id = 'google-contacts-gsi' 47 | js.src = 'https://accounts.google.com/gsi/client' 48 | if (firstJs && firstJs.parentNode) { 49 | firstJs.parentNode.insertBefore(js, firstJs) 50 | } else { 51 | document.head.appendChild(js) 52 | } 53 | js.onload = this.loadClient 54 | } 55 | } 56 | 57 | loadApi() { 58 | const { apiKey } = this.props 59 | 60 | window.gapi.load('client', () => { 61 | window.gapi.client.init({ 62 | apiKey 63 | }) 64 | }) 65 | } 66 | 67 | loadClient() { 68 | const { clientId, hostedDomain, loginHint, accessType, onFailure, redirectUri, uxMode } = this.props 69 | 70 | if (accessType === 'online') { 71 | this.client = window.google.accounts.oauth2.initTokenClient({ 72 | client_id: clientId, 73 | scope: SCOPE, 74 | hosted_domain: hostedDomain, 75 | hint: loginHint 76 | }) 77 | } 78 | 79 | if (accessType === 'offline') { 80 | this.client = window.google.accounts.oauth2.initCodeClient({ 81 | client_id: clientId, 82 | scope: SCOPE, 83 | hosted_domain: hostedDomain, 84 | hint: loginHint, 85 | redirect_uri: redirectUri, 86 | ux_mode: uxMode 87 | }) 88 | } 89 | 90 | this.client.error_callback = onFailure 91 | } 92 | 93 | handleImportContacts(tokenResponse, pageToken = null) { 94 | const { onFailure, maxResults } = this.props 95 | 96 | if (tokenResponse) { 97 | window.gapi.client 98 | .request({ 99 | path: 'https://people.googleapis.com/v1/people/me/connections', 100 | params: { 101 | personFields: 'names,emailAddresses', 102 | pageSize: maxResults > 1000 ? 1000 : maxResults, 103 | ...(pageToken && { pageToken }) 104 | } 105 | }) 106 | .then( 107 | response => this.handleNextDataFetch(response, tokenResponse), 108 | err => onFailure(err) 109 | ) 110 | } 111 | } 112 | 113 | handleNextDataFetch(response, authResponse) { 114 | const { maxResults } = this.props 115 | // Parse the response body 116 | const parsedData = JSON.parse(response.body) 117 | // Store the fetched data so that we can use it later 118 | this.allData = [...this.allData, ...parsedData.connections] 119 | // If we have more data and the number of data we fethced is less than maxResults then fetch again using the nextPageToken 120 | 121 | if ('nextPageToken' in parsedData && maxResults > this.allData.length) { 122 | this.handleImportContacts(authResponse, parsedData.nextPageToken) 123 | } else { 124 | this.handleParseContacts() 125 | } 126 | } 127 | 128 | handleParseContacts() { 129 | const { onSuccess, onFailure } = this.props 130 | const results = [] 131 | try { 132 | for (let index = 0; index < this.allData.length; index += 1) { 133 | const element = this.allData[index] 134 | 135 | if (element.emailAddresses && element.emailAddresses.length > 0) { 136 | results.push({ 137 | email: element.emailAddresses[0].value, 138 | title: 'names' in element ? element.names[0].displayName : element.emailAddresses[0].value 139 | }) 140 | } 141 | } 142 | onSuccess(results) 143 | } catch (error) { 144 | onFailure('Error to fetch contacts') 145 | } 146 | } 147 | 148 | signIn(e) { 149 | this.allData = [] 150 | const { disable } = this.state 151 | const { prompt, onRequest, accessType, onSuccess } = this.props 152 | 153 | onRequest() 154 | 155 | if (e) { 156 | e.preventDefault() // to prevent submit if used within form 157 | } 158 | 159 | if (!disable) { 160 | if (accessType === 'online') { 161 | this.client.callback = resp => { 162 | this.handleImportContacts(resp) 163 | } 164 | 165 | if (window.gapi.client.getToken() === null) { 166 | this.client.requestAccessToken({ prompt }) 167 | } else { 168 | this.client.requestAccessToken({ prompt: '' }) 169 | } 170 | } 171 | 172 | if (accessType === 'offline') { 173 | this.client.callback = onSuccess 174 | 175 | if (window.gapi.client.getToken() === null) { 176 | this.client.requestCode({ prompt }) 177 | } else { 178 | this.client.requestCode({ prompt: '' }) 179 | } 180 | } 181 | } 182 | } 183 | 184 | render() { 185 | const { tag, type, className, disabledStyle, buttonText, children, render, theme, icon, disabled: disabledProps } = this.props 186 | const { active, hovered, disabled: disabledState } = this.state 187 | const disabled = disabledState || disabledProps 188 | 189 | if (render) { 190 | return render({ onClick: this.signIn }) 191 | } 192 | 193 | const initialStyle = { 194 | backgroundColor: theme === 'dark' ? 'rgb(66, 133, 244)' : '#fff', 195 | display: 'inline-flex', 196 | alignItems: 'center', 197 | color: theme === 'dark' ? '#fff' : 'rgba(0, 0, 0, .54)', 198 | boxShadow: '0 2px 2px 0 rgba(0, 0, 0, .24), 0 0 1px 0 rgba(0, 0, 0, .24)', 199 | padding: 0, 200 | borderRadius: 2, 201 | border: '1px solid transparent', 202 | fontSize: 14, 203 | fontWeight: '500', 204 | fontFamily: 'Roboto, sans-serif' 205 | } 206 | 207 | const hoveredStyle = { 208 | cursor: 'pointer', 209 | opacity: 0.9 210 | } 211 | 212 | const activeStyle = { 213 | cursor: 'pointer', 214 | backgroundColor: theme === 'dark' ? '#3367D6' : '#eee', 215 | color: theme === 'dark' ? '#fff' : 'rgba(0, 0, 0, .54)', 216 | opacity: 1 217 | } 218 | 219 | const defaultStyle = (() => { 220 | if (disabled) { 221 | return Object.assign({}, initialStyle, disabledStyle) 222 | } 223 | 224 | if (active) { 225 | if (theme === 'dark') { 226 | return Object.assign({}, initialStyle, activeStyle) 227 | } 228 | 229 | return Object.assign({}, initialStyle, activeStyle) 230 | } 231 | 232 | if (hovered) { 233 | return Object.assign({}, initialStyle, hoveredStyle) 234 | } 235 | 236 | return initialStyle 237 | })() 238 | const googleLoginButton = React.createElement( 239 | tag, 240 | { 241 | onMouseEnter: () => this.setState({ hovered: true }), 242 | onMouseLeave: () => this.setState({ hovered: false, active: false }), 243 | onMouseDown: () => this.setState({ active: true }), 244 | onMouseUp: () => this.setState({ active: false }), 245 | onClick: this.signIn, 246 | style: defaultStyle, 247 | type, 248 | disabled, 249 | className 250 | }, 251 | [ 252 | icon && , 253 | 254 | {children || buttonText} 255 | 256 | ] 257 | ) 258 | 259 | return googleLoginButton 260 | } 261 | } 262 | 263 | GoogleContacts.propTypes = { 264 | accessType: PropTypes.string, 265 | buttonText: PropTypes.node, 266 | children: PropTypes.node, 267 | className: PropTypes.string, 268 | clientId: PropTypes.string.isRequired, 269 | disabled: PropTypes.bool, 270 | disabledStyle: PropTypes.object, 271 | hostedDomain: PropTypes.string, 272 | icon: PropTypes.bool, 273 | loginHint: PropTypes.string, 274 | maxResults: PropTypes.number, 275 | onFailure: PropTypes.func.isRequired, 276 | onRequest: PropTypes.func, 277 | onSuccess: PropTypes.func.isRequired, 278 | prompt: PropTypes.string, 279 | redirectUri: PropTypes.string, 280 | render: PropTypes.func, 281 | tag: PropTypes.string, 282 | theme: PropTypes.string, 283 | type: PropTypes.string, 284 | uxMode: PropTypes.string 285 | } 286 | 287 | GoogleContacts.defaultProps = { 288 | accessType: 'online', 289 | buttonText: 'Import from Gmail', 290 | disabled: false, 291 | disabledStyle: { 292 | opacity: 0.6 293 | }, 294 | icon: true, 295 | maxResults: 999, 296 | onRequest: () => {}, 297 | prompt: 'consent', 298 | tag: 'button', 299 | theme: 'light', 300 | type: 'button', 301 | uxMode: 'popup' 302 | } 303 | 304 | export default GoogleContacts 305 | -------------------------------------------------------------------------------- /src/icon.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Icon({ active }) { 4 | return ( 5 |
13 | 14 | 15 | 19 | 20 | 24 | 28 | 29 | 30 | 31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './google-contacts' 2 | -------------------------------------------------------------------------------- /webpack/uglify.json: -------------------------------------------------------------------------------- 1 | { 2 | "uglifyOptions": { 3 | "ie8": false, 4 | "debug": false, 5 | "sourceMap": false, 6 | "warnings": false, 7 | "compress": { 8 | "unsafe_comps": true, 9 | "properties": true, 10 | "keep_fargs": false, 11 | "pure_getters": true, 12 | "collapse_vars": true, 13 | "unsafe": true, 14 | "sequences": true, 15 | "dead_code": true, 16 | "drop_debugger": true, 17 | "comparisons": true, 18 | "conditionals": true, 19 | "evaluate": true, 20 | "booleans": true, 21 | "loops": true, 22 | "unused": true, 23 | "hoist_funs": true, 24 | "if_return": true, 25 | "join_vars": true, 26 | "drop_console": false, 27 | "pure_funcs": ["classCallCheck", "invariant", "warning"] 28 | }, 29 | 30 | "parallel": true, 31 | "output": { 32 | "comments": false 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /webpack/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const fileRoot = process.cwd() 5 | 6 | module.exports = { 7 | target: 'web', 8 | mode: 'development', 9 | devtool: 'eval', 10 | entry: { 11 | app: ['react-hot-loader/patch', 'webpack/hot/only-dev-server', './demo/index.js'] 12 | }, 13 | output: { 14 | path: path.join(fileRoot, 'demo'), 15 | filename: 'bundle.js', 16 | publicPath: '/' 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.js$/, 22 | use: { 23 | loader: 'babel-loader', 24 | options: { 25 | cacheDirectory: true, 26 | babelrc: false, 27 | presets: [ 28 | '@babel/preset-react', 29 | [ 30 | '@babel/preset-env', 31 | { 32 | targets: { 33 | esmodules: false 34 | } 35 | } 36 | ] 37 | ], 38 | plugins: ['react-hot-loader/babel'] 39 | } 40 | } 41 | } 42 | ] 43 | }, 44 | plugins: [ 45 | new webpack.HotModuleReplacementPlugin(), 46 | new webpack.DefinePlugin({ 47 | 'process.env.NODE_ENV': JSON.stringify('development') 48 | }) 49 | ], 50 | resolve: { 51 | extensions: ['*', '.js'] 52 | }, 53 | devServer: { 54 | contentBase: path.join(fileRoot, 'demo'), 55 | historyApiFallback: true, 56 | compress: false, 57 | host: process.env.IP || '0.0.0.0', 58 | port: parseInt(process.env.PORT, 0) || 8080, 59 | hot: true, 60 | open: false, 61 | quiet: false, 62 | noInfo: false, 63 | inline: true, 64 | lazy: false, 65 | headers: { 66 | 'Access-Control-Allow-Origin': '*', 67 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 68 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization' 69 | }, 70 | stats: { 71 | colors: true 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /webpack/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const TerserPlugin = require('terser-webpack-plugin') 4 | 5 | const fileRoot = process.cwd() 6 | 7 | module.exports = { 8 | mode: 'production', 9 | entry: './src/index.js', 10 | output: { 11 | path: path.join(fileRoot, 'dist'), 12 | filename: 'google-contacts.js', 13 | libraryTarget: 'umd', 14 | globalObject: 'typeof self !== "undefined" ? self : this', 15 | library: 'GoogleContacts' 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.js$/, 21 | use: { 22 | loader: 'babel-loader', 23 | options: { 24 | cacheDirectory: true, 25 | babelrc: false, 26 | presets: [ 27 | '@babel/preset-react', 28 | [ 29 | '@babel/preset-env', 30 | { 31 | targets: { 32 | esmodules: false 33 | } 34 | } 35 | ] 36 | ], 37 | plugins: ['transform-react-remove-prop-types'] 38 | } 39 | } 40 | } 41 | ] 42 | }, 43 | externals: { 44 | react: 'react', 45 | 'react-dom': 'ReactDOM' 46 | }, 47 | resolve: { 48 | extensions: ['.js'] 49 | }, 50 | optimization: { 51 | minimize: true, 52 | minimizer: [new TerserPlugin()] 53 | }, 54 | plugins: [ 55 | new webpack.DefinePlugin({ 56 | 'process.env.NODE_ENV': JSON.stringify('production') 57 | }), 58 | new webpack.LoaderOptionsPlugin({ 59 | minimize: true, 60 | debug: false 61 | }), 62 | new webpack.optimize.AggressiveMergingPlugin(), 63 | new webpack.optimize.ModuleConcatenationPlugin() 64 | ], 65 | performance: { 66 | hints: 'warning' 67 | }, 68 | stats: { 69 | errorDetails: true, 70 | assets: true, 71 | children: false, 72 | chunks: false, 73 | hash: false, 74 | modules: false, 75 | publicPath: false, 76 | timings: true, 77 | version: false, 78 | warnings: true, 79 | colors: { 80 | green: '\u001b[32m' 81 | } 82 | } 83 | } 84 | --------------------------------------------------------------------------------