Third party calendar integration
Overview
The Calendar Integration is an optional plugin, that includes synchronization with your Google and Outlook calendar services. It can be installed and used with the Mobiscroll Event Calendar as described below.
Currently, the calendar integration plugins cannot be used in Chrome extensions, because the CSP rules do not allow loading scripts from 3rd party domains.
Installing the Calendar Integration Plugin
When your Mobiscroll package is created with the Download Builder and you have access to the Calendar Integration, you can choose to include it in the built package. In this case you will need to make sure you checked the Calendar Integration before downloading the package from the download page. After installing your downloaded package, the Calendar Integration will be available for import.
When you're using the full Mobiscroll package from NPM, then you have the possibility to install the Calendar Integration as an additional package (also from NPM). In this case, after successfully using the Mobiscroll CLI to configure the project, you will have to install the @mobiscroll/calendar-integration
package from npm. Use the following command:
npm install @mobiscroll/calendar-integration
Google calendar integration
The Google Calendar Integration is a part of the third party calendar integrations plugin that manages the synchronization with your Google calendar services.
Public google calendars
Calling the init
function will do the necessary initializations for the third party. After the init, you can list the events from the public calendar.
import { googleCalendarSync } from "@mobiscroll/calendar-integration";
const calInst = mobiscroll.eventcalendar('#myDiv'. {
view: { schedule: { type: 'week' }},
});
// init google client
googleCalendarSync.init({
apiKey: 'YOUR_APY_KEY',
onInit: () => {
googleCalendarSync.getEvents(
'PUBLIC_CALENDAR_ID',
new Date(2022, 1, 1),
new Date(2022, 3, 0)
).then((events) => {
calInst.setEvents(events);
});
},
});
Private google calendars
Calling the init
function will do the necessary initializations for the third party. For this step you need to use an API key and a client ID. After the init
, you can sign in, list your calendars and events and create, update or delete the events on the calendars you have permission to.
import { googleCalendarSync } from "@mobiscroll/calendar-integration";
const calInst = mobiscroll.eventcalendar('#myDiv'. {
view: { schedule: { type: 'week' }},
});
// init google client
googleCalendarSync.init({
apiKey: 'YOUR_APY_KEY',
clientId: 'YOUR_CLIENT_ID',
onSignedIn: () => {
googleCalendarSync.getEvents(
['MY_FIRST_CALENDAR_ID', 'MY_SECOND_CALENDAR_ID'],
new Date(2022, 1, 1),
new Date(2022, 3, 0)
).then((events) => {
calInst.setEvents(events);
});
},
onSignedOut: () => {
calInst.setEvents([]);
},
});
Server side tokens
By default the authentication happens entirely on the client side. However, since the introduction of the new Google Identity Services, the received access token, which ensures access to the user's calendars, is only valid for an hour. After expiry, the user will be prompted again for consent.
You can refresh an access token without prompting the user for permission, but this needs to be done server side. To enable this, in the init config object set the auth option to 'server'
, and specify the authUrl
and refreshUrl
pointing to your server endpoints.
The authUrl
endpoint will receive a POST request, containing a unique authorization code. To exchange an authorization code for an access token, send a POST request to the https://oauth2.googleapis.com/token endpoint and set the following parameters:
client_id
- The client ID obtained from the Google API Console Credentials page.client_secret
- The client secret obtained from the Google API Console Credentials page.code
- The received authorization code.grant_type
- This field's value must be set toauthorization_code
.redirect_uri
- This field's value must be set topostmessage
.
POST /token HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded
code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7&
client_id=your_client_id&
client_secret=your_client_secret&
redirect_uri=postmessage&
grant_type=authorization_code
{
"access_token": "1/fFAGRNJru1FTz70BzhT3Zg",
"expires_in": 3599,
"token_type": "Bearer",
"scope": "https://www.googleapis.com/auth/calendar.events.public.readonly https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events.owned",
"refresh_token": "1//xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI"
}
Return the received response from the request.
The refreshUrl
endpoint will also receive a POST request, containing the refresh token, received earlier. To refresh an access token, send a POST request to the https://oauth2.googleapis.com/token endpoint and set the following parameters:
client_id
- The client ID obtained from the Google API Console Credentials page.client_secret
- The client secret obtained from the Google API Console Credentials page.refresh_token
- The received refresh token.grant_type
- This field's value must be set torefresh_token
.
POST /token HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded
client_id=your_client_id&
client_secret=your_client_secret&
refresh_token=your_refresh_token&
grant_type=refresh_token
{
"access_token": "1/fFAGRNJru1FTz70BzhT3Zg",
"expires_in": 3599,
"token_type": "Bearer",
"scope": "https://www.googleapis.com/auth/calendar.events.public.readonly https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events.owned"
}
Return the received response from the request.
Complete example
A complete example with Node.js, ASP.NET and PHP
- Node.js
- ASP.NET
- PHP
// Client
import { googleCalendarSync } from '@mobiscroll/calendar-integration';
googleCalendarSync.init({
auth: 'server',
authUrl: 'http://example.com/auth',
clientId: 'YOUR_CLIENT_ID',
refreshUrl: 'http://example.com/refresh',
});
// Server
const http = require('http');
const https = require('https');
const YOUR_CLIENT_ID = 'YOUR_CLIENT_ID';
const YOUR_CLIENT_SECRET = 'YOUR_CLIENT_SECRET';
function getToken(type, codeOrToken, callback) {
const postData =
'client_id=' + YOUR_CLIENT_ID + '&' +
'client_secret=' + YOUR_CLIENT_SECRET + '&' +
(type === 'refresh' ?
'grant_type=refresh_token&' +
'refresh_token=' + codeOrToken
:
'grant_type=authorization_code&' +
'code=' + codeOrToken + '&' +
'redirect_uri=postmessage&' +
'code_verifier='
)
const postOptions = {
host: 'oauth2.googleapis.com',
port: '443',
path: '/token',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData)
}
};
const postReq = https.request(postOptions, function (response) {
response.setEncoding('utf8');
response.on('data', d => {
callback(d);
});
});
postReq.on('error', (error) => {
console.log(error)
});
// Post the request with data
postReq.write(postData);
postReq.end();
}
function getPostData(req, callback) {
let body = '';
req.on('data', (data) => {
body += data;
});
req.on('end', () => {
const parsed = new URLSearchParams(body);
const data = {}
for (const pair of parsed.entries()) {
data[pair[0]] = pair[1];
}
callback(data);
});
}
function checkCSRF(req, res) {
// Check if CSRF header is present
if (req.headers['x-requested-with'] === 'XmlHttpRequest') {
return true;
}
// Otherwise end the request
res.statusCode = 500;
res.end();
return false;
}
function sendResponse(res, data) {
// Set the headers in case of CORS request
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With');
// Send data
res.end(data);
}
const server = http.createServer(function (req, res) {
if (req.method === 'OPTIONS') { // Handle preflight request (in case of CORS request)
res.setHeader('Access-Control-Allow-Origin', '*'); // Use your own domain instead of the '*'
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With');
res.end();
} else if (req.url.startsWith('/auth')) { // Handle auth
if (checkCSRF(req, res)) {
getPostData(req, (data) => {
// Exchange auth code to access token (on sign in)
getToken('auth', data.code, (token) => {
sendResponse(res, token);
});
});
}
} else if (req.url.startsWith('/refresh')) { // Handle refresh
if (checkCSRF(req, res)) {
getPostData(req, (data) => {
// Exchange refresh token to access token (on access token expiry)
getToken('refresh', data.refresh_token, (token) => {
sendResponse(res, token);
});
});
}
}
});
server.listen(8080);
class HttpServer
{
public static HttpListener listener;
public static string url = "http://localhost:8080/";
private static readonly HttpClient client = new HttpClient();
public static Dictionary GetKeyValuePairs(string data)
{
return data.Split('&')
.Select(value => value.Split('='))
.ToDictionary(pair => pair[0], pair => pair[1]);
}
public static void SendResponse(HttpListenerRequest request, HttpListenerResponse resp, string type)
{
if (!request.HasEntityBody)
{
resp.Close();
}
Stream body = request.InputStream;
StreamReader reader = new StreamReader(body, request.ContentEncoding);
string codeOrToken = type == "auth" ? GetKeyValuePairs(reader.ReadToEnd())["code"] : GetKeyValuePairs(reader.ReadToEnd())["refresh_token"];
string postData = "client_id=" + YOUR_CLIENT_ID + "&" +
"client_secret=" + YOUR_CLIENT_SECRET + "&" +
(type == "refresh" ?
"grant_type=refresh_token&" +
"refresh_token=" + codeOrToken
:
"grant_type=authorization_code&" +
"code=" + codeOrToken + "&" +
"redirect_uri=postmessage&" +
"code_verifier=");
// Set the headers in case of CORS request
resp.AppendHeader("Access-Control-Allow-Origin", "*");
resp.AppendHeader("Access-Control-Allow-Headers", "X-Requested-With");
// Post the request with data
FormUrlEncodedContent content = new FormUrlEncodedContent(GetKeyValuePairs(postData));
HttpResponseMessage response = client.PostAsync("https://oauth2.googleapis.com/token", content).Result;
if (response.IsSuccessStatusCode)
{
HttpContent responseContent = response.Content;
string responseString = responseContent.ReadAsStringAsync().Result;
byte[] buffer = Encoding.UTF8.GetBytes(responseString);
// Get a response stream and write the response to it
resp.ContentLength64 = buffer.Length;
Stream output = resp.OutputStream;
output.Write(buffer, 0, buffer.Length);
output.Close();
}
else
{
Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}
resp.Close();
}
public static bool CheckCSRF(HttpListenerRequest req, HttpListenerResponse resp)
{
// Check if CSRF header is present
if (req.Headers["x-requested-with"] == "XmlHttpRequest")
{
return true;
}
// Otherwise end the request
resp.StatusCode = 500;
resp.Close();
return false;
}
public static async Task HandleIncomingConnections()
{
bool runServer = true;
// Handling requests
while (runServer)
{
HttpListenerContext ctx = await listener.GetContextAsync();
HttpListenerRequest req = ctx.Request;
HttpListenerResponse resp = ctx.Response;
if (req.HttpMethod == "OPTIONS")
{ // Handle preflight request (in case of CORS request)
resp.AppendHeader("Access-Control-Allow-Origin", "*"); // Use your own domain instead of the '*'
resp.AppendHeader("Access-Control-Allow-Headers", "X-Requested-With");
resp.Close();
}
else if (req.Url.ToString().Contains("/auth"))
{ // Handle auth
if (CheckCSRF(req, resp))
{
SendResponse(req, resp, "auth");
}
}
else if (req.Url.ToString().Contains("/refresh"))
{ // Handle refresh
if (CheckCSRF(req, resp))
{
SendResponse(req, resp, "refresh");
}
}
}
}
public static void Main()
{
// Create a Http server and start listening for incoming connections
listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8080/");
listener.Start();
// Handle requests
Task listenTask = HandleIncomingConnections();
listenTask.GetAwaiter().GetResult();
// Close the listener
listener.Close();
}
}
function checkCSRF()
{
// Check if CSRF header is present
if ($_SERVER['HTTP_X_REQUESTED_WITH'] === 'XmlHttpRequest') {
return true;
}
// Otherwise end the request
header("HTTP/1.1 500 Internal Server Error");
return false;
}
function sendResponse($type)
{
// Set the headers in case of CORS request
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: X-Requested-With');
// The data you want to send via POST
$post_data = [
"client_id" => $YOUR_CLIENT_ID,
"client_secret" => $YOUR_CLIENT_SECRET,
"grant_type" => $type === 'refresh' ? "refresh_token" : "authorization_code",
"refresh_token" => $type === 'refresh' ? $_POST['refresh_token'] : "",
"code" => $type === 'refresh' ? "" : $_POST['code'],
"redirect_uri" => "postmessage",
"code_verifier" => ""
];
// url-ify the data for the POST
$data_string = http_build_query($post_data);
// Open connection
$ch = curl_init();
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/x-www-form-urlencoded',
'Content-Length: ' . strlen($data_string),
));
// Set the url, number of POST vars, POST data
curl_setopt($ch, CURLOPT_URL, "https://oauth2.googleapis.com/token");
curl_setopt($ch, CURLOPT_PORT, 443);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
// SSL options are set for testing purposes
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
// So that curl_exec returns the contents of the cURL; rather than echoing it
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Execute post
$result = curl_exec($ch);
if (curl_errno($ch)) {
print curl_error($ch);
}
echo $result;
curl_close($ch);
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { // Handle preflight request (in case of CORS request)
header('Access-Control-Allow-Origin: *'); // Use your own domain instead of the '*'
header('Access-Control-Allow-Headers: X-Requested-With');
} else if (htmlspecialchars($_GET["action"]) === 'auth') { // Handle auth
if (checkCSRF()) {
sendResponse('auth');
}
} else if (htmlspecialchars($_GET["action"]) === 'refresh') { // Handle refresh
if (checkCSRF()) {
sendResponse('refresh');
}
}
API
Configuration options
apiKey
string
The API Key obtained from the Google API Console Credentials page.
auth
"client" | "server"
When set to 'server'
, server-side endpoints must be implemented to get the auth access token from Google.
authUrl
string
Server side endpoint for receiving an access token from Google on sign in, when the auth
option is set to 'server'
.
clientId
string
The client ID obtained from the Google API Console Credentials page.
gapi
any
The gapi object, if already loaded. If not specified, the library will load it.
gis
any
The Google Identity Services client library, if already loaded. If not specified, the library will load it.
refreshUrl
string
Server side endpoint for receiving a new access token from Google on expiry, when the auth
option is set to 'server'
.
scopes
string
Specify custom scopes for Google authentication.
The default scopes are
'https://www.googleapis.com/auth/calendar.events.public.readonly https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events.owned'
timezone
string
If specified, the event dates will be returned in this timezone.
timezonePlugin
The timezone plugin, needed if timezone is specified.
The MbscTimezonePlugin
type has the following properties:
createDate
: (s: any, year: string | number | Date | MbscTimezonedDate, month: number, date: number, h: number, min: number, sec: number, ms: number) => MbscTimezonedDate -parse
: (date: string | number, s: any) => MbscTimezonedDate -
Events
onInit
() => void
Callback executed when the library is initialized and ready to use.
onSignedIn
() => void
Callback executed when the user signed in.
onSignedOut
() => void
Callback executed when the user signed out.
Methods
addEvent
(calendarId: string, event: MbscCalendarEvent, callback: (addedEvent: MbscCalendarEvent) => void ) => Promise
Adds an event to the specified calendar.
googleCalendarSync.addEvent(
'MY_CALENDAR_ID',
{
start: new Date(2022, 1, 15, 12),
end: new Date(2022, 1, 16, 14),
title: 'My new event',
googleEvent: {
description: 'My new event description'
}
});
Parameters:
calendarId - The ID of the calendar
event - The event to add. You can pass Google specific event properties through the
googleEvent
property. The rest of custom properties will be passed to theextendedProperties
field.callback - Callback function which is executed when the request is complete. Receives the added event.
deleteEvent
(calendarId: string, event: MbscCalendarEvent, callback: (deletedEvent: MbscCalendarEvent) => void ) => Promise
Removes an event in the specified calendar.
Parameters:
calendarId - The ID of the calendar
event - The event to delete
callback - Callback function which is executed then the request is complete. Receives the deleted event.
getCalendars
(callback: (calendars: Array<any>) => void ) => Promise
Queries the available calendars of the signed in user. Calls the callback function, if specified.
Parameters:
- callback - A callback function to call with the calendars as parameters, when the query finished.
getEvents
(calendarIds: string | Array<string>, start: Date, end: Date, callback: (events: Array<MbscCalendarEvent>) => void ) => Promise
Queries the events of the specified calendars between two dates.
Parameters:
calendarIds - Array of the calendar IDs.
start - Start date of the specified interval.
end - End date of the specified interval.
callback - Callback function which is executed then the request is complete. Receives the list of events as parameter.
init
(config: MbscGoogleCalendarSyncConfig) => void
Makes the necessary initializations for the 3rd party.
Triggers the onInit
event when the initialization is ready, if specified.
Parameters:
- config - The configuration object for the calendar integration
isSignedIn
() => boolean
Checks if the user is signed in or not.
signIn
() => Promise
If the user is not signed in, starts the sign in flow. On success, triggers the onSignedIn
event.
signOut
() => Promise
If the user is signed in, signs out. On success triggers the onSignedOut
event.
updateEvent
(calendarId: string, event: MbscCalendarEvent, callback: (updatedEvent: MbscCalendarEvent) => void ) => Promise
Updates an event in the specified calendar.
googleCalendarSync.updateEvent(
'MY_CALENDAR_ID',
{
start: new Date(2022, 1, 20, 10),
end: new Date(2022, 1, 11, 15),
title: 'My updated event',
id: 1,
googleEvent: {
description: 'My updated event description'
}
});
Parameters:
calendarId - The ID of the calendar
event - The event to update. You can pass Google specific event properties through the
googleEvent
property. The rest of custom properties will be passed to theextendedProperties
field.callback - Callback function which is executed then the request is complete. Receives the updated event.
Outlook calendar integration
The Outlook Calendar Integration is a part of the third party calendar integrations plugin that manages the synchronization with your Outlook calendar services.
Outlook calendars
Calling the init
function will do the necessary initializations for the third party. For this step you need to use a client ID. After the init, you can sign in, list your calendars and events and create, update or delete the events on the calendars you have permission to.
import { outlookCalendarSync} from "@mobiscroll/calendar-integration";
const calInst = mobiscroll.eventcalendar('#myDiv'. {
view: { schedule: { type: 'week' }},
});
// init outlook client
outlookCalendarSync.init({
clientId: 'YOUR_CLIENT_ID',
onSignedIn: () => {
outlookCalendarSync.getEvents(
['MY_FIRST_CALENDAR_ID', 'MY_SECOND_CALENDAR_ID'],
new Date(2022, 1, 1),
new Date(2022, 3, 0)
).then((events) => {
calInst.setEvents(events);
});
},
onSignedOut: () => {
calInst.setEvents([]);
},
});
API
Configuration options
clientId
string
The client ID obtained from the Outlook web app.
msal
any
The Microsoft Authentication Library, if already loaded. If not specified, the library will load it.
msalClient
any
The instance of the client application, if already loaded. If not specified, the library will load it.
pageSize
number
The maximum number of events to retrieve with one request. Default value is 1000
.
redirectUri
string
The location where the authorization server sends the user once the app has been successfully authorized.
Default value is 'http://localhost:3000'
.
timezone
string
If specified, the event dates will be returned in this timezone.
timezonePlugin
The timezone plugin, needed if timezone is specified.
The MbscTimezonePlugin
type has the following properties:
createDate
: (s: any, year: string | number | Date | MbscTimezonedDate, month: number, date: number, h: number, min: number, sec: number, ms: number) => MbscTimezonedDate -parse
: (date: string | number, s: any) => MbscTimezonedDate -
Events
onInit
() => void
Callback executed when the library is initialized and ready to use.
onSignedIn
() => void
Callback executed when the user signed in.
onSignedOut
() => void
Callback executed when the user signed out.
Methods
addEvent
(calendarId: string, event: MbscCalendarEvent, callback: (addedEvent: MbscCalendarEvent) => void ) => Promise
Adds an event to the specified calendar.
outlookCalendarSync.addEvent(
'MY_CALENDAR_ID',
{
start: new Date(2022, 1, 15, 12),
end: new Date(2022, 1, 16, 14),
title: 'My new event',
outlookEvent: {
isReminderOn: true,
}
});
Parameters:
calendarId - The ID of the calendar
event - The event to add. You can pass Outlook specific event properties through the
outlookEvent
property.callback - Callback function which is executed when the request is complete. Receives the added event.
deleteEvent
(calendarId: string, event: MbscCalendarEvent, callback: (deletedEvent: MbscCalendarEvent) => void ) => Promise
Removes an event in the specified calendar.
Parameters:
calendarId - The ID of the calendar
event - The event to delete
callback - Callback function which is executed then the request is complete. Receives the deleted event.
getCalendars
(callback: (calendars: Array<any>) => void ) => Promise
Queries the available calendars of the signed in user. Calls the callback function, if specified.
Parameters:
- callback - A callback function to call with the calendars as parameters, when the query finished.
getEvents
(calendarIds: string | Array<string>, start: Date, end: Date, callback: (events: Array<MbscCalendarEvent>) => void ) => Promise
Queries the events of the specified calendars between two dates.
Parameters:
calendarIds - Array of the calendar IDs.
start - Start date of the specified interval.
end - End date of the specified interval.
callback - Callback function which is executed then the request is complete. Receives the list of events as parameter.
init
(config: MbscOutlookCalendarSyncConfig) => void
Makes the necessary initializations for the 3rd party.
Triggers the onInit
event when the initialization is ready, if specified.
Parameters:
- config - The configuration object for the calendar integration
isSignedIn
() => boolean
Checks if the user is signed in or not.
signIn
() => Promise
If the user is not signed in, starts the sign in flow. On success, triggers the onSignedIn
event.
signOut
() => Promise
If the user is signed in, signs out. On success triggers the onSignedOut
event.
updateEvent
(calendarId: string, event: MbscCalendarEvent, callback: (updatedEvent: MbscCalendarEvent) => void ) => Promise
Updates an event in the specified calendar.
outlookCalendarSync.updateEvent(
'MY_CALENDAR_ID',
{
start: new Date(2022, 1, 20, 10),
end: new Date(2022, 1, 11, 15),
title: 'My updated event',
id: 1,
outlookEvent: {
isReminderOn: false,
}
});
Parameters:
calendarId - The ID of the calendar
event - The event to update. You can pass Outlook specific event properties through the
outlookEvent
property.callback - Callback function which is executed when the request is complete. Receives the updated event.