├── .gitignore ├── .travis.yml ├── Gulpfile.js ├── README.md ├── angular-digits ├── bower.json ├── dist └── angular-digits.js ├── karma.conf.js ├── package.json ├── src ├── digits-response-error.js ├── digits-response.js ├── digits.js └── provider.js └── test ├── polyfill.js └── unit └── digits.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | .idea/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '0.10' 5 | - '0.11' 6 | - '0.12' 7 | - '4.0' 8 | - '4.1' 9 | - '4.2' 10 | - iojs 11 | 12 | cache: 13 | directories: 14 | - node_modules 15 | - bower_components 16 | 17 | before_script: 18 | - npm install 19 | - bower install 20 | 21 | notifications: 22 | slack: 23 | secure: KO5rOSJsJF+7Hbte0uWZvyjqtL8Au7Ka9uIm0DKEsW2/ktuUUiN3YOnjcowjc/f/XZeESryHyJ11itsc2NimTVvXu+yFxd6DFklx0PuMhTDztklPjaGEysSussMZ1hZJ8RUDBIBbH56FwTSqlbtOu046z9i0tcwXw89DwQnt+GE= 24 | -------------------------------------------------------------------------------- /Gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | concat = require('gulp-concat'), 3 | karma = require('karma').server; 4 | 5 | gulp.task('test', function (done) { 6 | karma.start({ 7 | configFile: __dirname + '/karma.conf.js', 8 | singleRun: true 9 | }, done); 10 | }); 11 | 12 | gulp.task('build', function () { 13 | gulp.src([ 14 | 'src/digits.js', 15 | 'src/provider.js', 16 | 'src/digits-response.js', 17 | 'src/digits-response-error.js' 18 | ]) 19 | .pipe(concat('angular-digits.js')) 20 | .pipe(gulp.dest('dist/')); 21 | }); 22 | 23 | gulp.task('default', ['build']); 24 | gulp.task('watch', function () { 25 | gulp.watch('src/*.js', ['build']); 26 | }) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-digits 2 | 3 | This is an angular wrapper for [twitter fabric's digits sdk](https://dev.twitter.com/twitter-kit/web/digits). It bridges the gap between Angular's digest cycle and the foreign SDK by applying the asynchronous events to the digest cycle. It also provides convenient models to provide insight to the responses from the API (if the workflow was closed by the user, popup was blocked, etc). 4 | 5 | 6 | ## Example usage 7 | 8 | ```js 9 | angular.module('app', ['atticoos.digits']) 10 | 11 | .config(function (DigitsProvider) { 12 | // configure the provider before the application's run phase 13 | DigitsProvider.setConsumerKey('XXX'); 14 | }) 15 | 16 | .run(function (Digits) { 17 | var options = { 18 | accountFields: Digits.AccountFields.Email // Access Email Addresses 19 | }; 20 | 21 | 22 | Digits.login(options).then(function (loginResponse) { 23 | // successfully logged in 24 | }).catch(function (error) { 25 | // a decorated error object describing the issue logging in 26 | if (error.wasPopupBlocked()) { 27 | // popup blocked 28 | } else if (error.wasWindowClosed()) { 29 | // user closed the window 30 | } else { 31 | // something else 32 | } 33 | }); 34 | }); 35 | ``` 36 | 37 | ## Login Options 38 | 39 | You can customize the Digits for Web sign in flow to collect user email addresses or pre-fill phone number information, based on your app’s needs. 40 | 41 | For more informations go to [Digits SDK Sign in Options Doc](https://docs.fabric.io/web/digits/sign-in-options.html#callback-url) 42 | 43 | ## Response Objects 44 | This wrapper comes with some convenient models for better transparency of state and response information from the Digits SDK. It is suggested by the [Digits SDK Docs](https://dev.twitter.com/twitter-kit/web/digits) that you securely verify the response data when logging in. 45 | 46 | ### Login Response 47 | ```js 48 | Digits.login().then(function (loginResponse) { }); 49 | ``` 50 | #### loginResponse.getOAuthHeaders() 51 | Returns: `Object` 52 | 53 | Returns the headers used to perform verification of the login response (`X-Verify-Credentials-Authorization` and `X-Auth-Service-Provider`) 54 | ```js 55 | { 56 | authorization: 'xx', 57 | url: 'https://xx' 58 | } 59 | ``` 60 | 61 | ### Error Response 62 | ```js 63 | Digits.login().catch(function (loginError) { }); 64 | ``` 65 | 66 | #### loginError.wasWindowClosed() 67 | Returns: `Boolean` 68 | 69 | Indicates if the workflow was aborted directly by the user closing the window before the process had completed 70 | 71 | #### loginError.wasPopupBlocked() 72 | Returns: `Boolean` 73 | 74 | Indicates if the browser prevented the workflow by blocking the popup 75 | 76 | 77 | #### loginError.type 78 | The `type` attribute from the original response object 79 | 80 | #### loginError.message 81 | The `message` attribute from the original response object 82 | -------------------------------------------------------------------------------- /angular-digits: -------------------------------------------------------------------------------- 1 | angular-digits -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-digits", 3 | "version": "1.1.0", 4 | "authors": [ 5 | "Atticus White " 6 | ], 7 | "ignore": [], 8 | "description": "AngularJS module for Twitter's Fabric Digits", 9 | "main": "index.js", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "angular-mocks": "~1.3.15", 13 | "angular": "~1.3.15" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /dist/angular-digits.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('atticoos.digits', []) 5 | .run(['$window', 'Digits', function ($window, Digits) { 6 | $window.Digits.init({consumerKey: Digits.getConsumerKey()}); 7 | }]); 8 | }).apply(this); 9 | 10 | (function () { 11 | 'use strict'; 12 | /* https://dev.twitter.com/twitter-kit/web/digits */ 13 | function DigitsProvider () { 14 | var consumerKey; 15 | 16 | /** 17 | * @name setConsumreKey 18 | * @public 19 | * 20 | * @description 21 | * Sets the Twitter public consumer key to be used with the Digits SDK 22 | */ 23 | this.setConsumerKey = function (key) { 24 | consumerKey = key; 25 | }; 26 | 27 | this.$get = [ 28 | '$rootScope', 29 | '$log', 30 | '$q', 31 | '$window', 32 | 'DigitsResponseError', 33 | 'DigitsResponse', 34 | function ($rootScope, $log, $q, $window, DigitsLoginError, DigitsResponse) { 35 | var service = {}, 36 | conditionalApply; 37 | 38 | conditionalApply = function (execution) { 39 | if ($rootScope.$$phase) { 40 | execution(); 41 | } else { 42 | $rootScope.$apply(execution); 43 | } 44 | }; 45 | 46 | service.getConsumerKey = function () { 47 | return consumerKey; 48 | }; 49 | 50 | /** 51 | * @name login 52 | * @public 53 | * 54 | * @description 55 | * Prompts the login popup and resolve the response 56 | */ 57 | service.login = function (options) { 58 | var deferred = $q.defer(); 59 | 60 | options = angular.isDefined(options) ? options : {}; 61 | 62 | $window.Digits.logIn(options) 63 | .done(function (response) { 64 | conditionalApply(function () { 65 | deferred.resolve(new DigitsResponse(response)); 66 | }); 67 | }) 68 | .fail(function (error) { 69 | conditionalApply(function () { 70 | deferred.reject(new DigitsLoginError(error)); 71 | }); 72 | }); 73 | return deferred.promise; 74 | }; 75 | 76 | /** 77 | * Checks if the user is already loggd in 78 | */ 79 | service.isLoggedIn = function () { 80 | var deferred = $q.defer(); 81 | $window.Digits.getLoginStatus() 82 | .done(function (response) { 83 | conditionalApply(function () { 84 | if (response.status === 'authorized') { 85 | deferred.resolve(response.status); 86 | } else { 87 | deferred.reject(response.status); 88 | } 89 | }); 90 | }) 91 | .fail(function () { 92 | conditionalApply(function () { 93 | deferred.reject(); 94 | }); 95 | }); 96 | return deferred.promise; 97 | }; 98 | 99 | return service; 100 | } 101 | ]; 102 | } 103 | angular.module('atticoos.digits') 104 | .provider('Digits', [DigitsProvider]); 105 | }).apply(this); 106 | 107 | (function () { 108 | 'use strict'; 109 | 110 | function DigitsResponse () { 111 | var HEADER = { 112 | AUTHORIZATION: 'X-Verify-Credentials-Authorization', 113 | URL: 'X-Auth-Service-Provider' 114 | }; 115 | 116 | function Response (responseObject) { 117 | angular.forEach(responseObject, function (value, property) { 118 | Object.defineProperty(this, property, { 119 | enumerable: true, 120 | configurable: false, 121 | get: function () { 122 | return value; 123 | } 124 | }); 125 | }.bind(this)); 126 | } 127 | 128 | Response.prototype.getOAuthHeaders = function () { 129 | return { 130 | authorization: this.oauth_echo_headers[HEADER.AUTHORIZATION], 131 | url: this.oauth_echo_headers[HEADER.URL] 132 | }; 133 | }; 134 | 135 | return Response; 136 | } 137 | 138 | angular.module('atticoos.digits') 139 | .factory('DigitsResponse', [DigitsResponse]); 140 | }).apply(this); 141 | 142 | (function () { 143 | 'use strict'; 144 | 145 | function DigitsResponseError () { 146 | var TYPE = { 147 | ABORT: 'abort', 148 | BLOCKED: 'popup_blocker' 149 | }; 150 | 151 | function Response (responseObject) { 152 | this.type = responseObject.type; 153 | this.message = responseObject.message; 154 | } 155 | 156 | Response.prototype.wasWindowClosed = function () { 157 | return this.type === TYPE.ABORT; 158 | }; 159 | 160 | Response.prototype.wasPopupBlocked = function () { 161 | return this.type === TYPE.BLOCKED; 162 | }; 163 | 164 | return Response; 165 | 166 | } 167 | 168 | angular.module('atticoos.digits') 169 | .factory('DigitsResponseError', [DigitsResponseError]); 170 | 171 | }).apply(this); 172 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | browsers: ['PhantomJS'], 4 | frameworks: ['jasmine'], 5 | files: [ 6 | 'https://cdn.digits.com/1/sdk.js', 7 | 'bower_components/angular/angular.js', 8 | 'bower_components/angular-mocks/angular-mocks.js', 9 | 'test/polyfill.js', 10 | 'src/digits.js', 11 | 'src/provider.js', 12 | 'src/digits-response.js', 13 | 'src/digits-response-error.js', 14 | 'test/**/*.spec.js' 15 | ], 16 | reporters: ['dots'] 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-digits", 3 | "version": "1.1.0", 4 | "description": "Twitter digits for AngularJS", 5 | "main": "dist/angular-digits.js", 6 | "scripts": { 7 | "test": "gulp test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/ajwhite/angular-digits.git" 12 | }, 13 | "keywords": [ 14 | "angularjs", 15 | "twitter", 16 | "fabric", 17 | "digits", 18 | "angular-digits" 19 | ], 20 | "author": "Atticus White (http://atticuswhite.com/)", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/ajwhite/angular-digits/issues" 24 | }, 25 | "homepage": "https://github.com/ajwhite/angular-digits", 26 | "devDependencies": { 27 | "bower": "^1.3.12", 28 | "gulp": "^3.8.11", 29 | "gulp-concat": "^2.5.2", 30 | "jasmine-core": "^2.2.0", 31 | "jscs": "^1.12.0", 32 | "jshint": "^2.6.3", 33 | "karma": "^0.12.31", 34 | "karma-jasmine": "^0.3.5", 35 | "karma-phantomjs-launcher": "^0.1.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/digits-response-error.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | function DigitsResponseError () { 5 | var TYPE = { 6 | ABORT: 'abort', 7 | BLOCKED: 'popup_blocker' 8 | }; 9 | 10 | function Response (responseObject) { 11 | this.type = responseObject.type; 12 | this.message = responseObject.message; 13 | } 14 | 15 | Response.prototype.wasWindowClosed = function () { 16 | return this.type === TYPE.ABORT; 17 | }; 18 | 19 | Response.prototype.wasPopupBlocked = function () { 20 | return this.type === TYPE.BLOCKED; 21 | }; 22 | 23 | return Response; 24 | 25 | } 26 | 27 | angular.module('atticoos.digits') 28 | .factory('DigitsResponseError', [DigitsResponseError]); 29 | 30 | }).apply(this); 31 | -------------------------------------------------------------------------------- /src/digits-response.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | function DigitsResponse () { 5 | var HEADER = { 6 | AUTHORIZATION: 'X-Verify-Credentials-Authorization', 7 | URL: 'X-Auth-Service-Provider' 8 | }; 9 | 10 | function Response (responseObject) { 11 | angular.forEach(responseObject, function (value, property) { 12 | Object.defineProperty(this, property, { 13 | enumerable: true, 14 | configurable: false, 15 | get: function () { 16 | return value; 17 | } 18 | }); 19 | }.bind(this)); 20 | } 21 | 22 | Response.prototype.getOAuthHeaders = function () { 23 | return { 24 | authorization: this.oauth_echo_headers[HEADER.AUTHORIZATION], 25 | url: this.oauth_echo_headers[HEADER.URL] 26 | }; 27 | }; 28 | 29 | return Response; 30 | } 31 | 32 | angular.module('atticoos.digits') 33 | .factory('DigitsResponse', [DigitsResponse]); 34 | }).apply(this); 35 | -------------------------------------------------------------------------------- /src/digits.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('atticoos.digits', []) 5 | .run(['$window', 'Digits', function ($window, Digits) { 6 | $window.Digits.init({consumerKey: Digits.getConsumerKey()}); 7 | }]); 8 | }).apply(this); 9 | -------------------------------------------------------------------------------- /src/provider.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | /* https://dev.twitter.com/twitter-kit/web/digits */ 4 | function DigitsProvider () { 5 | var consumerKey; 6 | 7 | /** 8 | * @name setConsumreKey 9 | * @public 10 | * 11 | * @description 12 | * Sets the Twitter public consumer key to be used with the Digits SDK 13 | */ 14 | this.setConsumerKey = function (key) { 15 | consumerKey = key; 16 | }; 17 | 18 | this.$get = [ 19 | '$rootScope', 20 | '$log', 21 | '$q', 22 | '$window', 23 | 'DigitsResponseError', 24 | 'DigitsResponse', 25 | function ($rootScope, $log, $q, $window, DigitsLoginError, DigitsResponse) { 26 | var service = {}, 27 | conditionalApply; 28 | 29 | conditionalApply = function (execution) { 30 | if ($rootScope.$$phase) { 31 | execution(); 32 | } else { 33 | $rootScope.$apply(execution); 34 | } 35 | }; 36 | 37 | service.getConsumerKey = function () { 38 | return consumerKey; 39 | }; 40 | 41 | /** 42 | * @name login 43 | * @public 44 | * 45 | * @description 46 | * Prompts the login popup and resolve the response 47 | */ 48 | service.login = function (options) { 49 | var deferred = $q.defer(); 50 | 51 | options = angular.isDefined(options) ? options : {}; 52 | 53 | $window.Digits.logIn(options) 54 | .done(function (response) { 55 | conditionalApply(function () { 56 | deferred.resolve(new DigitsResponse(response)); 57 | }); 58 | }) 59 | .fail(function (error) { 60 | conditionalApply(function () { 61 | deferred.reject(new DigitsLoginError(error)); 62 | }); 63 | }); 64 | return deferred.promise; 65 | }; 66 | 67 | /** 68 | * Checks if the user is already loggd in 69 | */ 70 | service.isLoggedIn = function () { 71 | var deferred = $q.defer(); 72 | $window.Digits.getLoginStatus() 73 | .done(function (response) { 74 | conditionalApply(function () { 75 | if (response.status === 'authorized') { 76 | deferred.resolve(response.status); 77 | } else { 78 | deferred.reject(response.status); 79 | } 80 | }); 81 | }) 82 | .fail(function () { 83 | conditionalApply(function () { 84 | deferred.reject(); 85 | }); 86 | }); 87 | return deferred.promise; 88 | }; 89 | 90 | return service; 91 | } 92 | ]; 93 | } 94 | angular.module('atticoos.digits') 95 | .provider('Digits', [DigitsProvider]); 96 | }).apply(this); 97 | -------------------------------------------------------------------------------- /test/polyfill.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * Polyfill for `Function.prototype.bind` 6 | */ 7 | if (!Function.prototype.bind) { 8 | Function.prototype.bind = function(oThis) { 9 | if (typeof this !== 'function') { 10 | throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); 11 | } 12 | var aArgs = Array.prototype.slice.call(arguments, 1); 13 | var fToBind = this; 14 | var FNOP = function() {}; 15 | var fBound = function() { 16 | return fToBind.apply(this instanceof FNOP && oThis ? this : oThis, 17 | aArgs.concat(Array.prototype.slice.call(arguments))); 18 | }; 19 | FNOP.prototype = this.prototype; 20 | fBound.prototype = new FNOP(); 21 | return fBound; 22 | }; 23 | } 24 | })(); 25 | -------------------------------------------------------------------------------- /test/unit/digits.spec.js: -------------------------------------------------------------------------------- 1 | describe('atticoos.digits', function () { 2 | var Digits; 3 | 4 | beforeEach(module('atticoos.digits', function (DigitsProvider) { 5 | DigitsProvider.setConsumerKey('xxx'); 6 | })); 7 | 8 | beforeEach(inject(function (_Digits_) { 9 | Digits = _Digits_; 10 | })); 11 | 12 | it ('should exist', function () { 13 | expect(Digits).toBeDefined(); 14 | }) 15 | }); 16 | --------------------------------------------------------------------------------