• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!--
2Copyright 2014 Google Inc
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    https://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15-->
16
17<link rel="import" href="../polymer/polymer.html">
18<link rel="import" href="../google-apis/google-js-api.html">
19
20<script>
21  (function() {
22
23    /**
24     * Enum of attributes to be passed through to the login API call.
25     * @readonly
26     * @enum {string}
27     */
28    var ProxyLoginAttributes = {
29      'appPackageName': 'apppackagename',
30      'clientId': 'clientid',
31      'cookiePolicy': 'cookiepolicy',
32      'hostedDomain': 'hostedDomain',
33      'openidPrompt': 'prompt',
34      'requestVisibleActions': 'requestvisibleactions'
35    };
36
37    /**
38     * AuthEngine does all interactions with gapi.auth2
39     *
40     * It is tightly coupled with <google-signin-aware> element
41     * The elements configure AuthEngine.
42     * AuthEngine propagates all authentication events to all google-signin-aware elements
43     *
44     * API used: https://developers.google.com/identity/sign-in/web/reference
45     *
46     */
47    var AuthEngine = {
48
49      /**
50       * oauth2 argument, set by google-signin-aware
51       */
52      _clientId: null,
53
54      get clientId() {
55        return this._clientId;
56      },
57
58      set clientId(val) {
59        if (this._clientId && val && val != this._clientId) {
60          throw new Error('clientId cannot change. Values do not match. New: ' + val + ' Old:' + this._clientId);
61        }
62        if (val && val != this._clientId) {
63          this._clientId = val;
64          this.initAuth2();
65        }
66      },
67
68      /**
69       * oauth2 argument, set by google-signin-aware
70       */
71      _cookiePolicy: 'single_host_origin',
72
73      get cookiePolicy() {
74        return this._cookiePolicy;
75      },
76
77      set cookiePolicy(val) {
78        if (val) {
79          this._cookiePolicy = val;
80        }
81      },
82
83      /**
84       * oauth2 argument, set by google-signin-aware
85       */
86      _appPackageName: '',
87
88      get appPackageName() {
89        return this._appPackageName;
90      },
91
92      set appPackageName(val) {
93        if (this._appPackageName && val && val != this._appPackageName) {
94          throw new Error('appPackageName cannot change. Values do not match. New: ' + val + ' Old: ' + this._appPackageName);
95        }
96        if (val) {
97          this._appPackageName = val;
98        }
99      },
100
101     /**
102       * oauth2 argument, set by google-signin-aware
103       */
104      _requestVisibleActions: '',
105
106      get requestVisibleactions() {
107        return this._requestVisibleActions;
108      },
109
110      set requestVisibleactions(val) {
111        if (this._requestVisibleActions && val && val != this._requestVisibleActions) {
112          throw new Error('requestVisibleactions cannot change. Values do not match. New: ' + val + ' Old: ' + this._requestVisibleActions);
113        }
114        if (val)
115          this._requestVisibleActions = val;
116      },
117
118     /**
119       * oauth2 argument, set by google-signin-aware
120       */
121      _hostedDomain: '',
122
123      get hostedDomain() {
124        return this._hostedDomain;
125      },
126
127      set hostedDomain(val) {
128        if (this._hostedDomain && val && val != this._hostedDomain) {
129          throw new Error('hostedDomain cannot change. Values do not match. New: ' + val + ' Old: ' + this._hostedDomain);
130        }
131        if (val)
132          this._hostedDomain = val;
133      },
134
135     /**
136       * oauth2 argument, set by google-signin-aware
137       */
138      _openidPrompt: '',
139
140      get openidPrompt() {
141        return this._openidPrompt;
142      },
143
144      set openidPrompt(val) {
145        if (typeof val !== 'string') {
146          throw new Error(
147              'openidPrompt must be a string. Received ' + typeof val);
148        }
149        if (val) {
150          var values = val.split(' ');
151          values = values.map(function(v) {
152            return v.trim();
153          });
154          values = values.filter(function(v) {
155            return v;
156          });
157          var validValues = {none: 0, login: 0, consent: 0, select_account: 0};
158          values.forEach(function(v) {
159            if (v == 'none' && values.length > 1) {
160              throw new Error(
161                  'none cannot be combined with other openidPrompt values');
162            }
163            if (!(v in validValues)) {
164              throw new Error(
165                  'invalid openidPrompt value ' + v +
166                  '. Valid values: ' + Object.keys(validValues).join(', '));
167            }
168          });
169        }
170        this._openidPrompt = val;
171      },
172
173      /** Is offline access currently enabled in the google-signin-aware element? */
174      _offline: false,
175
176      get offline() {
177        return this._offline;
178      },
179
180      set offline(val) {
181        this._offline = val;
182        this.updateAdditionalAuth();
183      },
184
185      /** Should we force a re-prompt for offline access? */
186      _offlineAlwaysPrompt: false,
187
188      get offlineAlwaysPrompt() {
189        return this._offlineAlwaysPrompt;
190      },
191
192      set offlineAlwaysPrompt(val) {
193        this._offlineAlwaysPrompt = val;
194        this.updateAdditionalAuth();
195      },
196
197      /** Have we already gotten offline access from Google during this session? */
198      offlineGranted: false,
199
200      /** <google-js-api> */
201      _apiLoader: null,
202
203      /** an array of wanted scopes. oauth2 argument */
204      _requestedScopeArray: [],
205
206      /** _requestedScopeArray as string */
207      get requestedScopes() {
208        return this._requestedScopeArray.join(' ');
209      },
210
211      /** Is auth library initalized? */
212      _initialized: false,
213
214      /** Is user signed in? */
215      _signedIn: false,
216
217      /** Currently granted scopes */
218      _grantedScopeArray: [],
219
220      /** True if additional authorization is required */
221      _needAdditionalAuth: true,
222
223      /** True if have google+ scopes */
224      _hasPlusScopes: false,
225
226      /**
227       * array of <google-signin-aware>
228       * state changes are broadcast to them
229       */
230      signinAwares: [],
231
232      init: function() {
233        this._apiLoader = document.createElement('google-js-api');
234        this._apiLoader.addEventListener('js-api-load', this.loadAuth2.bind(this));
235        if (Polymer.Element) {
236          document.body.appendChild(this._apiLoader);
237        }
238      },
239
240      loadAuth2: function() {
241        gapi.load('auth2', this.initAuth2.bind(this));
242      },
243
244      initAuth2: function() {
245        if (!('gapi' in window) || !('auth2' in window.gapi) || !this.clientId) {
246          return;
247        }
248        var auth = gapi.auth2.init({
249          'client_id': this.clientId,
250          'cookie_policy': this.cookiePolicy,
251          'scope': this.requestedScopes,
252          'hosted_domain': this.hostedDomain
253        });
254
255        auth['currentUser'].listen(this.handleUserUpdate.bind(this));
256
257        auth.then(
258          function onFulfilled() {
259          // Let the current user listener trigger the changes.
260          },
261          function onRejected(error) {
262            console.error(error);
263          }
264        );
265      },
266
267      handleUserUpdate: function(newPrimaryUser) {
268        // update and broadcast currentUser
269        var isSignedIn = newPrimaryUser.isSignedIn();
270        if (isSignedIn != this._signedIn) {
271          this._signedIn = isSignedIn;
272          for (var i=0; i<this.signinAwares.length; i++) {
273            this.signinAwares[i]._setSignedIn(isSignedIn);
274          }
275        }
276        // update and broadcast initialized property the first time the isSignedIn property is set.
277        if(!this._initialized) {
278            for (var i=0; i<this.signinAwares.length; i++) {
279                this.signinAwares[i]._setInitialized(true);
280            }
281            this._initialized = true;
282        }
283
284
285        // update granted scopes
286        this._grantedScopeArray = this.strToScopeArray(
287          newPrimaryUser.getGrantedScopes());
288        // console.log(this._grantedScopeArray);
289        this.updateAdditionalAuth();
290
291        var response = newPrimaryUser.getAuthResponse();
292        for (var i=0; i<this.signinAwares.length; i++) {
293          this.signinAwares[i]._updateScopeStatus(response);
294        }
295      },
296
297      setOfflineCode: function(code) {
298        for (var i=0; i<this.signinAwares.length; i++) {
299          this.signinAwares[i]._updateOfflineCode(code);
300        }
301      },
302
303      /** convert scope string to scope array */
304      strToScopeArray: function(str) {
305        if (!str) {
306          return [];
307        }
308        // remove extra spaces, then split
309        var scopes = str.replace(/\ +/g, ' ').trim().split(' ');
310        for (var i=0; i<scopes.length; i++) {
311          scopes[i] = scopes[i].toLowerCase();
312           // Handle scopes that will be deprecated but are still returned with their old value
313          if (scopes[i] === 'https://www.googleapis.com/auth/userinfo.profile') {
314            scopes[i] = 'profile';
315          }
316          if (scopes[i] === 'https://www.googleapis.com/auth/userinfo.email') {
317            scopes[i] = 'email';
318          }
319        }
320        // return with duplicates filtered out
321        return scopes.filter( function(value, index, self) {
322          return self.indexOf(value) === index;
323        });
324      },
325
326      /** true if scopes have google+ scopes */
327      isPlusScope: function(scope) {
328        return (scope.indexOf('/auth/games') > -1)
329            || (scope.indexOf('auth/plus.') > -1 && scope.indexOf('auth/plus.me') < 0);
330      },
331
332      /** true if scopes have been granted */
333      hasGrantedScopes: function(scopeStr) {
334        var scopes = this.strToScopeArray(scopeStr);
335        for (var i=0; i< scopes.length; i++) {
336          if (this._grantedScopeArray.indexOf(scopes[i]) === -1)
337            return false;
338        }
339        return true;
340      },
341
342      /** request additional scopes */
343      requestScopes: function(newScopeStr) {
344        var newScopes = this.strToScopeArray(newScopeStr);
345        var scopesUpdated = false;
346        for (var i=0; i<newScopes.length; i++) {
347          if (this._requestedScopeArray.indexOf(newScopes[i]) === -1) {
348            this._requestedScopeArray.push(newScopes[i]);
349            scopesUpdated = true;
350          }
351        }
352        if (scopesUpdated) {
353          this.updateAdditionalAuth();
354          this.updatePlusScopes();
355        }
356      },
357
358      /** update status of _needAdditionalAuth */
359      updateAdditionalAuth: function() {
360        var needMoreAuth = false;
361        if ((this.offlineAlwaysPrompt || this.offline ) && !this.offlineGranted) {
362          needMoreAuth = true;
363        } else {
364          for (var i=0; i<this._requestedScopeArray.length; i++) {
365            if (this._grantedScopeArray.indexOf(this._requestedScopeArray[i]) === -1) {
366              needMoreAuth = true;
367              break;
368            }
369          }
370        }
371        if (this._needAdditionalAuth != needMoreAuth) {
372          this._needAdditionalAuth = needMoreAuth;
373          // broadcast new value
374          for (var i=0; i<this.signinAwares.length; i++) {
375            this.signinAwares[i]._setNeedAdditionalAuth(needMoreAuth);
376          }
377        }
378      },
379
380      updatePlusScopes: function() {
381        var hasPlusScopes = false;
382        for (var i = 0; i < this._requestedScopeArray.length; i++) {
383          if (this.isPlusScope(this._requestedScopeArray[i])) {
384            hasPlusScopes = true;
385            break;
386          }
387        }
388        if (this._hasPlusScopes != hasPlusScopes) {
389          this._hasPlusScopes = hasPlusScopes;
390          for (var i=0; i<this.signinAwares.length; i++) {
391            this.signinAwares[i]._setHasPlusScopes(hasPlusScopes);
392          }
393        }
394      },
395      /**
396       * attached <google-signin-aware>
397       * @param {!GoogleSigninAwareElement} aware element to add
398       */
399      attachSigninAware: function(aware) {
400        if (this.signinAwares.indexOf(aware) == -1) {
401          this.signinAwares.push(aware);
402          // Initialize aware properties
403          aware._setNeedAdditionalAuth(this._needAdditionalAuth);
404          aware._setInitialized(this._initialized);
405          aware._setSignedIn(this._signedIn);
406          aware._setHasPlusScopes(this._hasPlusScopes);
407        } else {
408          console.warn('signinAware attached more than once', aware);
409        }
410      },
411
412      detachSigninAware: function(aware) {
413        var index = this.signinAwares.indexOf(aware);
414        if (index != -1) {
415          this.signinAwares.splice(index, 1);
416        } else {
417          console.warn('Trying to detach unattached signin-aware');
418        }
419      },
420
421      /** returns scopes not granted */
422      getMissingScopes: function() {
423        return this._requestedScopeArray.filter( function(scope) {
424          return this._grantedScopeArray.indexOf(scope) === -1;
425        }.bind(this)).join(' ');
426      },
427
428      assertAuthInitialized: function() {
429        if (!this.clientId) {
430          throw new Error("AuthEngine not initialized. clientId has not been configured.");
431        }
432        if (!('gapi' in window)) {
433          throw new Error("AuthEngine not initialized. gapi has not loaded.");
434        }
435        if (!('auth2' in window.gapi)) {
436          throw new Error("AuthEngine not initialized. auth2 not loaded.");
437        }
438      },
439
440      /** pops up sign-in dialog */
441      signIn: function() {
442        this.assertAuthInitialized();
443        var params = {
444          'scope': this.getMissingScopes()
445        };
446
447        // Proxy specific attributes through to the signIn options.
448        Object.keys(ProxyLoginAttributes).forEach(function(key) {
449          if (this[key] && this[key] !== '') {
450            params[ProxyLoginAttributes[key]] = this[key];
451          }
452        }, this);
453
454        var promise;
455        var user = gapi.auth2.getAuthInstance()['currentUser'].get();
456        if (!(this.offline || this.offlineAlwaysPrompt)) {
457          if (user.getGrantedScopes()) {
458            // additional auth, skip multiple account dialog
459            promise = user.grant(params);
460          } else {
461            // initial signin
462            promise = gapi.auth2.getAuthInstance().signIn(params);
463          }
464        } else {
465          params.redirect_uri = 'postmessage';
466          if (this.offlineAlwaysPrompt) {
467            params.approval_prompt = 'force';
468          }
469
470          // Despite being documented at https://goo.gl/tiO0Bk
471          // It doesn't seem like user.grantOfflineAccess() actually exists in
472          // the current version of the Google Sign-In JS client we're using
473          // through GoogleWebComponents. So in the offline case, we will not
474          // distinguish between a first auth and an additional one.
475          promise = gapi.auth2.getAuthInstance().grantOfflineAccess(params);
476        }
477        promise.then(
478          function onFulfilled(response) {
479            // If login was offline, response contains one string "code"
480            // Otherwise it contains the user object already
481            var newUser;
482            if (response.code) {
483              AuthEngine.offlineGranted = true;
484              newUser = gapi.auth2.getAuthInstance()['currentUser'].get();
485              AuthEngine.setOfflineCode(response.code);
486            } else {
487              newUser = response;
488            }
489
490            var authResponse = newUser.getAuthResponse();
491            // Let the current user listener trigger the changes.
492          },
493          function onRejected(error) {
494            // Access denied is not an error, user hit cancel
495            if ("Access denied." !== error.reason) {
496              this.signinAwares.forEach(function(awareInstance) {
497                awareInstance.errorNotify(error);
498              });
499            }
500          }.bind(this)
501        );
502      },
503
504      /** signs user out */
505      signOut: function() {
506        this.assertAuthInitialized();
507        gapi.auth2.getAuthInstance().signOut().then(
508          function onFulfilled() {
509          // Let the current user listener trigger the changes.
510          },
511          function onRejected(error) {
512            console.error(error);
513          }
514        );
515      }
516    };
517
518    AuthEngine.init();
519
520/**
521`google-signin-aware` is used to enable authentication in custom elements by
522interacting with a google-signin element that needs to be present somewhere
523on the page.
524
525The `scopes` attribute allows you to specify which scope permissions are required
526(e.g do you want to allow interaction with the Google Drive API).
527
528The `google-signin-aware-success` event is triggered when a user successfully
529authenticates. If either `offline` or `offlineAlwaysPrompt` is set to true, successful
530authentication will also trigger the `google-signin-offline-success`event.
531The `google-signin-aware-signed-out` event is triggered when a user explicitly
532signs out via the google-signin element.
533
534You can bind to `isAuthorized` property to monitor authorization state.
535##### Example
536
537    <google-signin-aware scopes="https://www.googleapis.com/auth/drive"></google-signin-aware>
538
539
540##### Example with offline
541    <template id="awareness" is="dom-bind">
542      <google-signin-aware
543          scopes="https://www.googleapis.com/auth/drive"
544          offline
545          on-google-signin-aware-success="handleSignin"
546          on-google-signin-offline-success="handleOffline"></google-signin-aware>
547    <\/template>
548    <script>
549      var aware = document.querySelector('#awareness');
550      aware.handleSignin = function(response) {
551        var user = gapi.auth2.getAuthInstance()['currentUser'].get();
552        console.log('User name: ' + user.getBasicProfile().getName());
553      };
554      aware.handleOffline = function(response) {
555        console.log('Offline code received: ' + response.detail.code);
556        // Here you would POST response.detail.code to your webserver, which can
557        // exchange the authorization code for an access token. More info at:
558        // https://developers.google.com/identity/protocols/OAuth2WebServer
559      };
560    <\/script>
561*/
562    Polymer({
563
564      is: 'google-signin-aware',
565
566      /**
567       * Fired when this scope has been authorized
568       * @param {Object} result Authorization result.
569       * @event google-signin-aware-success
570       */
571
572      /**
573       * Fired when an offline authorization is successful.
574       * @param {{code: string}} detail -
575       *     code: The one-time authorization code from Google.
576       *         Your application can exchange this for an `access_token` and `refresh_token`
577       * @event google-signin-offline-success
578       */
579
580      /**
581       * Fired when this scope is not authorized
582       * @event google-signin-aware-signed-out
583       */
584
585      /**
586       * Fired when there is an error during the signin flow.
587       * @param {Object} detail The error object returned from the OAuth 2 flow.
588       * @event google-signin-aware-error
589       */
590
591      /**
592       * This block is needed so the previous @param is not assigned to the next property.
593       */
594
595      properties: {
596        /**
597         * App package name for android over-the-air installs.
598         * See the relevant [docs](https://developers.google.com/+/web/signin/android-app-installs)
599         */
600        appPackageName: {
601          type: String,
602          observer: '_appPackageNameChanged'
603        },
604
605        /**
606         * a Google Developers clientId reference
607         */
608        clientId: {
609          type: String,
610          observer: '_clientIdChanged'
611        },
612
613        /**
614         * The cookie policy defines what URIs have access to the session cookie
615         * remembering the user's sign-in state.
616         * See the relevant [docs](https://developers.google.com/+/web/signin/reference#determining_a_value_for_cookie_policy) for more information.
617         * @default 'single_host_origin'
618         */
619        cookiePolicy: {
620          type: String,
621          observer: '_cookiePolicyChanged'
622        },
623
624        /**
625         * The app activity types you want to write on behalf of the user
626         * (e.g http://schemas.google.com/AddActivity)
627         *
628         */
629        requestVisibleActions: {
630          type: String,
631          observer: '_requestVisibleActionsChanged'
632        },
633
634        /**
635         * The Google Apps domain to which users must belong to sign in.
636         * See the relevant [docs](https://developers.google.com/identity/sign-in/web/reference) for more information.
637         */
638        hostedDomain: {
639          type: String,
640          observer: '_hostedDomainChanged'
641        },
642
643       /**
644         * Allows for offline `access_token` retrieval during the signin process.
645         * See also `offlineAlwaysPrompt`. You only need to set one of the two; if both
646         * are set, the behavior of `offlineAlwaysPrompt` will override `offline`.
647         */
648        offline: {
649          type: Boolean,
650          value: false,
651          observer: '_offlineChanged'
652        },
653
654        /**
655          * Works the same as `offline` with the addition that it will always
656          * force a re-prompt to the user, guaranteeing that you will get a
657          * refresh_token even if the user has already granted offline access to
658          * this application. You only need to set one of `offline` or
659          * `offlineAlwaysPrompt`, not both.
660          */
661        offlineAlwaysPrompt: {
662          type: Boolean,
663          value: false,
664          observer: '_offlineAlwaysPromptChanged'
665        },
666
667       /**
668         * The scopes to provide access to (e.g https://www.googleapis.com/auth/drive)
669         * and should be space-delimited.
670         */
671        scopes: {
672          type: String,
673          value: 'profile',
674          observer: '_scopesChanged'
675        },
676
677        /**
678         * Space-delimited, case-sensitive list of strings that
679         * specifies whether the the user is prompted for reauthentication
680         * and/or consent. The defined values are:
681         *   none: do not display authentication or consent pages.
682         *     This value is mutually exclusive with the rest.
683         *   login: always prompt the user for reauthentication.
684         *   consent: always show consent screen.
685         *   select_account: always show account selection page.
686         *     This enables a user who has multiple accounts to select amongst
687         *     the multiple accounts that they might have current sessions for.
688         * For more information, see "prompt" parameter description in
689         * https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters
690         */
691        openidPrompt: {
692          type: String,
693          value: '',
694          observer: '_openidPromptChanged'
695        },
696
697        /**
698         * True when the auth library has been initialized, and signedIn property value is set from the first api response.
699         */
700        initialized: {
701            type: Boolean,
702            notify: true,
703            readOnly: true
704        },
705
706        /**
707         * True if user is signed in
708         */
709        signedIn: {
710          type: Boolean,
711          notify: true,
712          readOnly: true
713        },
714
715        /**
716         * True if authorizations for *this* element have been granted
717         */
718        isAuthorized: {
719          type: Boolean,
720          notify: true,
721          readOnly: true,
722          value: false
723        },
724
725        /**
726         * True if additional authorizations for *any* element are required
727         */
728        needAdditionalAuth: {
729          type: Boolean,
730          notify: true,
731          readOnly: true
732        },
733
734        /**
735         * True if *any* element has google+ scopes
736         */
737        hasPlusScopes: {
738          type: Boolean,
739          value: false,
740          notify: true,
741          readOnly: true
742        }
743      },
744
745      attached: function() {
746        AuthEngine.attachSigninAware(this);
747      },
748
749      detached: function() {
750        AuthEngine.detachSigninAware(this);
751      },
752
753      /** pops up the authorization dialog */
754      signIn: function() {
755        AuthEngine.signIn();
756      },
757
758      /** signs user out */
759      signOut: function() {
760        AuthEngine.signOut();
761      },
762
763      errorNotify: function(error) {
764        this.fire('google-signin-aware-error', error);
765      },
766
767      _appPackageNameChanged: function(newName, oldName) {
768        AuthEngine.appPackageName = newName;
769      },
770
771      _clientIdChanged: function(newId, oldId) {
772        AuthEngine.clientId = newId;
773      },
774
775      _cookiePolicyChanged: function(newPolicy, oldPolicy) {
776        AuthEngine.cookiePolicy = newPolicy;
777      },
778
779      _requestVisibleActionsChanged: function(newVal, oldVal) {
780        AuthEngine.requestVisibleActions = newVal;
781      },
782
783      _hostedDomainChanged: function(newVal, oldVal) {
784        AuthEngine.hostedDomain = newVal;
785      },
786
787      _offlineChanged: function(newVal, oldVal) {
788        AuthEngine.offline = newVal;
789      },
790
791      _offlineAlwaysPromptChanged: function(newVal, oldVal) {
792        AuthEngine.offlineAlwaysPrompt = newVal;
793      },
794
795      _scopesChanged: function(newVal, oldVal) {
796        AuthEngine.requestScopes(newVal);
797        this._updateScopeStatus(undefined);
798      },
799
800      _openidPromptChanged: function(newVal, oldVal) {
801        AuthEngine.openidPrompt = newVal;
802      },
803
804      _updateScopeStatus: function(user) {
805        var newAuthorized = this.signedIn && AuthEngine.hasGrantedScopes(this.scopes);
806        if (newAuthorized !== this.isAuthorized) {
807          this._setIsAuthorized(newAuthorized);
808          if (newAuthorized) {
809            this.fire('google-signin-aware-success', user);
810          }
811          else {
812            this.fire('google-signin-aware-signed-out', user);
813          }
814        }
815      },
816
817      _updateOfflineCode: function(code) {
818        if (code) {
819          this.fire('google-signin-offline-success', {code: code});
820        }
821      }
822    });
823  })();
824</script>
825