SharePoint Framework - Display Google Fit Information

In this article, we will explore how we can use Google Fit REST APIs in SPFx web part and develop a web part which helps to display the key fitness information (activity time spent, distance traveled, calories burned, step count) from the Google Fit data source.

Overview

 
Google Fitness REST APIs are useful if you have a fitness app and you want to integrate your data with Google Fit or if you just want to collect the fitness data and display some information to the users. The Google Fit REST APIs can be consumed in SharePoint Framework.
 
In this article, we will explore how we can use Google Fit REST APIs in SPFx web part and develop a web part to display the key fitness information from the Google Fit data source. We will use React JS in this example. For this article, I am using SharePoint Framework version 1.7.1
 

Create SPFx Solution

 
Open the command prompt. Create a directory for SPFx solution.
  1. md react-google-fit  
Navigate to the above-created directory.
  1. cd react-google-fit  
Run Yeoman SharePoint Generator to create the solution.
  1. yo @microsoft/sharepoint  
Yeoman generator will present you with the wizard by asking questions about the solution to be created.
 
SharePoint Framework - Display Google Fit Information
 
Solution Name: Hit Enter to have a default name (react-google-fit in this case) or type in any other name for your solution.
Selected choice: Hit Enter
 
Target for component: Here, we can select the target environment where we are planning to deploy the client web part, i.e., SharePoint Online or SharePoint OnPremise (SharePoint 2016 onwards).
Selected choice: SharePoint Online only (latest)
 
Place of files: We may choose to use the current folder or create a subfolder for our solution.
Selected choice: Use the current folder
 
Deployment option: We may choose to allow the tenant admin the choice of being able to deploy the solution to all sites immediately without running any feature deployment or adding apps in sites.
Selected choice: N (install on each site explicitly)
 
Type of client-side component to create: We can choose to create a client-side web part or an extension. Choose the web part option.
Selected choice: WebPart
 
Web part name: Hit Enter to select the default name or type in any other name.
Selected choice: GoogleFitActivityViewer
 
Web part description: Hit Enter to select the default description or type in any other value.
Selected choice: Display Google Fit Activities
 
Framework to use: Select any JavaScript framework to develop the component. Available choices are - No JavaScript Framework, React, and Knockout.
Selected choice: React
 
Yeoman generator will perform scaffolding process to generate the solution. The scaffolding process will take a significant amount of time.
 
Once the scaffolding process is completed, lock down the version of project dependencies by running the below command.
  1. npm shrinkwrap  
In the command prompt, type the below command to open the solution in a code editor of your choice.
  1. code .  

NPM Packages Used

 
react-google-authorize (https://www.npmjs.com/package/react-google-authorize)
 
This npm package helps to authenticate and authorize to Google.
 
Open “\src\webparts\googleFitActivityViewer\components\GoogleFitActivityViewer.tsx”.
 
Include the package.
  1. import { GoogleAuthorize } from 'react-google-authorize';  
Use the GoogleAuthorize component in render method.
  1. public render(): React.ReactElement<IGoogleFitActivityViewerProps> {  
  2.     const responseGoogle = (response) => {  
  3.     }  
  4.   
  5.     return (  
  6.       <div className={styles.googleFitActivityViewer}>  
  7.         <div className={styles.container}>  
  8.           {  
  9.             !this.state.isGoogleAuthenticated && this.state.accessToken == "" &&  
  10.             <GoogleAuthorize  
  11.               scope={'https://www.googleapis.com/auth/fitness.activity.read https://www.googleapis.com/auth/fitness.location.read'}  
  12.               clientId={this.props.clientId}  
  13.               onSuccess={responseGoogle}  
  14.               onFailure={responseGoogle}  
  15.             >  
  16.               <span>Login with Google</span>  
  17.             </GoogleAuthorize>  
  18.           }  
  19.         </div>  
  20.       </div>  
  21.     );  
  22. }  
We are including the below scopes.
  • https://www.googleapis.com/auth/fitness.activity.read - To read the fitness activities (calories burned, step count)
  • https://www.googleapis.com/auth/fitness.location.read - To read activity time spent, distance travelled

Define State

 
Create a new file IGoogleFitActivityViewerState.ts under “\src\webparts\googleFitActivityViewer\components\” folder.
  1. export interface IGoogleFitActivityViewerState {  
  2.     isGoogleAuthenticated: boolean;  
  3.     accessToken: string;  
  4.     stepCount: number;  
  5.     calories: number;  
  6.     distance: number;  
  7.     activityTime: number;  
  8. }  
2. Update your component “\src\webparts\googleFitActivityViewer\components\ GoogleFitActivityViewer.tsx” to use the state.
  1. import { IGoogleFitActivityViewerState } from './IGoogleFitActivityViewerState';  
  2.    
  3. export default class GoogleFitActivityViewer extends React.Component<IGoogleFitActivityViewerProps, IGoogleFitActivityViewerState> {  
  4.    
  5.   public constructor(props) {  
  6.     super(props);  
  7.   
  8.     this.state = {  
  9.       isGoogleAuthenticated: false,  
  10.       accessToken: "",  
  11.       stepCount: 0,  
  12.       calories: 0,  
  13.       distance: 0,  
  14.       activityTime: 0  
  15.     };  
  16.   }  
  17. }  

Implement the Service

 
We will create a service to query Google Fit REST APIs.
 

Interface to represent REST API data

 
Let us define an interface to represent the data returned by REST APIs.
 
Create a folder “services” under “src” folder and add a file IFitnessActivity.ts.
  1. export interface IFitnessActivity {  
  2.     dataSourceId: string;  
  3.     maxEndTimeNs: string;  
  4.     minStartTimeNs: string;  
  5.     point: IFitnessPoint[];  
  6. }  
  7.   
  8. export interface IFitnessPoint {  
  9.     dataTypeName: string;  
  10.     endTimeNanos: string;  
  11.     modifiedTimeMillis: string;  
  12.     value: IFitnessPointValue[];  
  13. }  
  14.   
  15. export interface IFitnessPointValue {  
  16.     intVal: number;  
  17.     fpVal: number;  
  18. }  
Implement Generic Interface
 
Add a file IDataService.ts under “\src\services” folder.
  1. export interface IDataService {  
  2.     getStepCount: (accessToken: string) => Promise<any>;  
  3.     getCalories: (accessToken: string) => Promise<any>;  
  4.     getDistance: (accessToken: string) => Promise<any>;  
  5.     getActivityTime: (accessToken: string) => Promise<any>;  
  6. }  
Implement Google Fit Interface 
 
Add a file GoogleFitService.ts under “\src\services” folder implementing IDataService interface. 
  1. import { ServiceScope, ServiceKey } from "@microsoft/sp-core-library";  
  2. import { IDataService } from './IDataService';  
  3. import { HttpClient, HttpClientResponse, IHttpClientOptions } from '@microsoft/sp-http';  
  4. import { PageContext } from '@microsoft/sp-page-context';  
  5. import { IFitnessActivity, IFitnessPoint, IFitnessPointValue } from './IFitnessActivity';  
  6.   
  7. export class GoogleFitService implements IDataService {  
  8.     public static readonly serviceKey: ServiceKey<IDataService> = ServiceKey.create<IDataService>('googleFit:data-service', GoogleFitService);  
  9.     private _httpClient: HttpClient;  
  10.     private _pageContext: PageContext;  
  11.   
  12.     constructor(serviceScope: ServiceScope) {  
  13.         serviceScope.whenFinished(() => {  
  14.             // Configure the required dependencies    
  15.             this._httpClient = serviceScope.consume(HttpClient.serviceKey);  
  16.             this._pageContext = serviceScope.consume(PageContext.serviceKey);  
  17.         });  
  18.     }  
  19.   
  20.     // Get step count from Google fit data source  
  21.     public getStepCount(accessToken: string): Promise<number> {  
  22.         return new Promise<number>((resolve: (itemId: number) => void, reject: (error: any) => void): void => {  
  23.             this.getGoogleFitData('derived:com.google.step_count.delta:com.google.android.gms:estimated_steps', accessToken)  
  24.                 .then((fitnessData: IFitnessActivity): void => {  
  25.                     var stepsCount: number = 0;  
  26.                     var i: number = 0;  
  27.                     var j: number = 0;  
  28.   
  29.                     // Calculate step count of each activity  
  30.                     for (i = 0; i < fitnessData.point.length; i++) {  
  31.                         for (j = 0; j < fitnessData.point[i].value.length; j++) {  
  32.                             stepsCount += fitnessData.point[i].value[j].intVal;  
  33.                         }  
  34.                     }  
  35.   
  36.                     resolve(stepsCount);  
  37.                 });  
  38.         });  
  39.     }  
  40.   
  41.     // Get calories burned from Google fit data source  
  42.     public getCalories(accessToken: string): Promise<number> {  
  43.         return new Promise<number>((resolve: (itemId: number) => void, reject: (error: any) => void): void => {  
  44.             this.getGoogleFitData('derived:com.google.calories.expended:com.google.android.gms:merge_calories_expended', accessToken)  
  45.                 .then((fitnessData: IFitnessActivity): void => {  
  46.                     var calories: number = 0;  
  47.                     var i: number = 0;  
  48.                     var j: number = 0;  
  49.   
  50.                     // Calculate calories burned during each activity  
  51.                     for (i = 0; i < fitnessData.point.length; i++) {  
  52.                         for (j = 0; j < fitnessData.point[i].value.length; j++) {  
  53.                             calories += fitnessData.point[i].value[j].fpVal;  
  54.                         }  
  55.                     }  
  56.   
  57.                     resolve(calories);  
  58.                 });  
  59.         });  
  60.     }  
  61.   
  62.     // Get distance travelled from Google fit data source  
  63.     public getDistance(accessToken: string): Promise<number> {  
  64.         return new Promise<number>((resolve: (itemId: number) => void, reject: (error: any) => void): void => {  
  65.             this.getGoogleFitData('derived:com.google.distance.delta:com.google.android.gms:merge_distance_delta', accessToken)  
  66.                 .then((fitnessData: IFitnessActivity): void => {  
  67.                     var distance: number = 0;  
  68.                     var i: number = 0;  
  69.                     var j: number = 0;  
  70.   
  71.                     // Calculate distance travelled during each activity  
  72.                     for (i = 0; i < fitnessData.point.length; i++) {  
  73.                         for (j = 0; j < fitnessData.point[i].value.length; j++) {  
  74.                             distance += fitnessData.point[i].value[j].fpVal;  
  75.                         }  
  76.                     }  
  77.   
  78.                     resolve(distance / 1000);  
  79.                 });  
  80.         });  
  81.     }  
  82.   
  83.     // Get activity time from Google fit data source  
  84.     public getActivityTime(accessToken: string): Promise<number> {  
  85.         return new Promise<number>((resolve: (itemId: number) => void, reject: (error: any) => void): void => {  
  86.             this.getGoogleFitData('derived:com.google.activity.segment:com.google.android.gms:merge_activity_segments', accessToken)  
  87.                 .then((fitnessData: IFitnessActivity): void => {  
  88.                     var activityTime: number = 0;  
  89.                     var i: number = 0;  
  90.                     var j: number = 0;  
  91.   
  92.                     // Calculate activity time spent for each activity  
  93.                     for (i = 0; i < fitnessData.point.length; i++) {  
  94.                         for (j = 0; j < fitnessData.point[i].value.length; j++) {  
  95.                             activityTime += fitnessData.point[i].value[j].intVal;  
  96.                         }  
  97.                     }  
  98.   
  99.                     resolve(activityTime);  
  100.                 });  
  101.         });  
  102.     }  
  103.   
  104.     // Get Google fit data by calling the REST API  
  105.     private getGoogleFitData(activityScope: string, accessToken: string): Promise<IFitnessActivity> {  
  106.         // Calculate start date, end date  
  107.         var startTime: number = new Date().getTime();  
  108.         var todayMidnight: Date = new Date();  
  109.         todayMidnight.setHours(0, 0, 0, 0);  
  110.         var endTime: number = todayMidnight.getTime();  
  111.   
  112.         const requestHeaders: Headers = new Headers();  
  113.         requestHeaders.append("Content-type""application/json");  
  114.         requestHeaders.append("Cache-Control""no-cache");  
  115.   
  116.         const postOptions: IHttpClientOptions = {  
  117.             headers: requestHeaders  
  118.         };  
  119.   
  120.         let sessionUrl: string = `https://www.googleapis.com/fitness/v1/users/me/dataSources/` + activityScope + `/datasets/` + startTime + `000000-` + endTime + `000000?access_token=` + accessToken;  
  121.         return new Promise<IFitnessActivity>((resolve: (itemId: IFitnessActivity) => void, reject: (error: any) => void): void => {  
  122.             this._httpClient.get(sessionUrl, HttpClient.configurations.v1, postOptions)  
  123.                 .then((response: HttpClientResponse) => {  
  124.                     response.json().then((responseJSON: IFitnessActivity) => {  
  125.                         resolve(responseJSON);  
  126.                     });  
  127.                 });  
  128.         });  
  129.     }  
  130. }  
Code the WebPart
 
Open the web part named GoogleFitActivityViewer.tsx under the “\src\webparts\googleFitActivityViewer\components\” folder. Here, implement the render method.
  1. public render(): React.ReactElement<IGoogleFitActivityViewerProps> {  
  2.   const responseGoogle = (response) => {  
  3.     this.setState(() => {  
  4.       return {  
  5.         ...this.state,  
  6.         isGoogleAuthenticated: true,  
  7.         accessToken: response.access_token  
  8.       };  
  9.     });  
  10.   
  11.     this.readStepCount(this.state.accessToken);  
  12.     this.readCalories(this.state.accessToken);  
  13.     this.readDistance(this.state.accessToken);  
  14.     this.readActivityTime(this.state.accessToken);  
  15.   };  
  16.   
  17.   const formatNumber = (num) => parseFloat(num.toFixed(2)).toLocaleString().replace(/\.([0-9])$/, ".$10");  
  18.   
  19.   return (  
  20.     <div className={styles.googleFitActivityViewer}>  
  21.       <div className={styles.container}>  
  22.         {  
  23.           !this.state.isGoogleAuthenticated && this.state.accessToken == "" &&  
  24.           <GoogleAuthorize  
  25.             scope={'https://www.googleapis.com/auth/fitness.activity.read https://www.googleapis.com/auth/fitness.location.read'}  
  26.             clientId={this.props.clientId}  
  27.             onSuccess={responseGoogle}  
  28.             onFailure={responseGoogle}  
  29.           >  
  30.             <span>Login with Google</span>  
  31.           </GoogleAuthorize>  
  32.         }  
  33.   
  34.         {  
  35.           this.state.isGoogleAuthenticated &&  
  36.           <div>  
  37.             <div className={styles.msTable}>  
  38.               <div className={styles.msTableRowHeader}>  
  39.                 <span className={styles.msTableCell}>  
  40.                   Today, {new Date().toDateString()}  
  41.                 </span>  
  42.               </div>  
  43.             </div>  
  44.   
  45.             <div className={styles.msTable}>  
  46.               <div className={styles.msTableRow}>  
  47.                 <span className={styles.msTableCell}>  
  48.                   <Icon iconName="Clock" className="ms-IconExample" />  
  49.                 </span>  
  50.                 <span className={styles.msTableCell}>  
  51.                   <b>{formatNumber(this.state.activityTime)}</b> min  
  52.               </span>  
  53.               </div>  
  54.   
  55.               <div className={styles.msTableRow}>  
  56.                 <span className={styles.msTableCell}>  
  57.                   <Icon iconName="POI" className="ms-IconExample" />  
  58.                 </span>  
  59.                 <span className={styles.msTableCell}>  
  60.                   <b>{formatNumber(this.state.distance)}</b> km  
  61.               </span>  
  62.               </div>  
  63.   
  64.               <div className={styles.msTableRow}>  
  65.                 <span className={styles.msTableCell}>  
  66.                   <Icon iconName="CaloriesAdd" className="ms-IconExample" />  
  67.                 </span>  
  68.                 <span className={styles.msTableCell}>  
  69.                   <b>{formatNumber(this.state.calories)}</b> calories  
  70.               </span>  
  71.               </div>  
  72.   
  73.               <div className={styles.msTableRow}>  
  74.                 <span className={styles.msTableCell}>  
  75.                   <Icon iconName="Running" className="ms-IconExample" />  
  76.                 </span>  
  77.                 <span className={styles.msTableCell}>  
  78.                   <b>{formatNumber(this.state.stepCount)}</b> steps  
  79.               </span>  
  80.               </div>  
  81.             </div>  
  82.           </div>  
  83.         }  
  84.       </div>  
  85.     </div>  
  86.   );  
  87. }  
Implement the helper methods and set the states from it.
  1. private readStepCount(accessToken: string): void {  
  2.   let serviceScope: ServiceScope = this.props.serviceScope;  
  3.   this.dataCenterServiceInstance = serviceScope.consume(GoogleFitService.serviceKey);  
  4.   
  5.   this.dataCenterServiceInstance.getStepCount(accessToken).then((stepCount: number) => {  
  6.     this.setState(() => {  
  7.       return {  
  8.         ...this.state,  
  9.         stepCount: stepCount  
  10.       };  
  11.     });  
  12.   });  
  13. }  
  14.   
  15. private readCalories(accessToken: string): void {  
  16.   let serviceScope: ServiceScope = this.props.serviceScope;  
  17.   this.dataCenterServiceInstance = serviceScope.consume(GoogleFitService.serviceKey);  
  18.   
  19.   this.dataCenterServiceInstance.getCalories(accessToken).then((calories: number) => {  
  20.     this.setState(() => {  
  21.       return {  
  22.         ...this.state,  
  23.         calories: calories  
  24.       };  
  25.     });  
  26.   });  
  27. }  
  28.   
  29. private readDistance(accessToken: string): void {  
  30.   let serviceScope: ServiceScope = this.props.serviceScope;  
  31.   this.dataCenterServiceInstance = serviceScope.consume(GoogleFitService.serviceKey);  
  32.   
  33.   this.dataCenterServiceInstance.getDistance(accessToken).then((distance: number) => {  
  34.     this.setState(() => {  
  35.       return {  
  36.         ...this.state,  
  37.         distance: distance  
  38.       };  
  39.     });  
  40.   });  
  41. }  
  42.   
  43. private readActivityTime(accessToken: string): void {  
  44.   let serviceScope: ServiceScope = this.props.serviceScope;  
  45.   this.dataCenterServiceInstance = serviceScope.consume(GoogleFitService.serviceKey);  
  46.   
  47.   this.dataCenterServiceInstance.getActivityTime(accessToken).then((activityTime: number) => {  
  48.     this.setState(() => {  
  49.       return {  
  50.         ...this.state,  
  51.         activityTime: activityTime  
  52.       };  
  53.     });  
  54.   });  
  55. }  

Add Authorized JavaScript Origins

 
Please refer to my previous article to generate OAuth 2.0 client ID.
  1. Open Google Developer Dashboard from here.
  2. Select the project created by following instructions from the previous article.
  3. From left navigation, click "Credentials".
  4. Click the listed web client.

    SharePoint Framework - Display Google Fit Information

  5. Under Authorized JavaScript origins, add SharePoint Online site URL (e.g. https://contoso.sharepoint.com ) or https://localhost:4321 if you are using SharePoint local workbench.

  6. Under Authorized redirect URI, add https://localhost:4321/auth/google/callback, if you are using SharePoint local workbench.

    SharePoint Framework - Display Google Fit Information

  7. Click "Save".

Configure the Web Part to use

  1.  Add "Google Fit Activity Viewer" web part on the SharePoint page.
  2. Edit the web part.
  3. Add the above generated OAuth 2.0 client ID to "ClientId Field" web part property.
  4. Save the changes.

    SharePoint Framework - Display Google Fit Information 

Summary

 
Google Fit REST APIs can be consumed in SharePoint Framework web part to display the key fitness information (activity time spent, distance traveled, calories burned, step count) from the Google fit data source. Npm package (react-google-authorize) helps in authenticating and authorizing the scopes in Google.