diff --git a/config.html b/config.html new file mode 100644 index 0000000..71e725d --- /dev/null +++ b/config.html @@ -0,0 +1,15 @@ + + + + + + + React • TodoMVC • Config + + + +
+ + + + \ No newline at end of file diff --git a/config/webpack.common.js b/config/webpack.common.js index 77d83fa..96355a8 100644 --- a/config/webpack.common.js +++ b/config/webpack.common.js @@ -6,6 +6,7 @@ var ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { entry: { 'app': './src/app.tsx', + 'config': './src/config.tsx', 'vendor': './src/vendor.ts' }, @@ -35,7 +36,11 @@ module.exports = { preLoaders: [ // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. - { test: /\.js$/, loader: 'source-map-loader' } + { + test: /\.js$/, + loader: 'source-map-loader', + exclude: /node_modules/ + } ] }, @@ -47,6 +52,13 @@ module.exports = { chunks: ['app', 'vendor'] }), + new HtmlWebpackPlugin({ + title: 'Config • TodoMVC', + filename: 'config.html', + template: path.resolve('config.html'), + chunks: ['config', 'vendor'] + }), + new ExtractTextPlugin('[name].css'), new webpack.ProvidePlugin({ diff --git a/index.html b/index.html index 3f653b8..3f35d11 100644 --- a/index.html +++ b/index.html @@ -17,6 +17,7 @@ + \ No newline at end of file diff --git a/package/manifest.dev.json b/package/manifest.dev.json new file mode 100644 index 0000000..d07c528 --- /dev/null +++ b/package/manifest.dev.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://statics.teams.microsoft.com/sdk/v0.2/manifest/MicrosoftTeams.schema.json", + "manifestVersion": "0.2", + "id": "com.example.microsoftteamstabs.tododev", + "version": "0.2", + "name": "ToDo [Dev]", + "developer": { + "name": "Example company", + "websiteUrl": "http://www.example.com", + "privacyUrl": "http://www.example.com/privacy", + "termsOfUseUrl": "http://www.example.com/termsofuse" + }, + "description": { + "short": "Create and manange your Outlook tasks.", + "full": "Create and manange your Outlook tasks. Sign in using your Office 365 work/school account, and click Save." + }, + "icons": { + "44": "todo44.png", + "88": "todo88.png" + }, + "accentColor": "#004578", + "configUrl": "https://localhost:3000/config.html", + "canUpdateConfig": true, + "needsIdentity": true, + "validDomains": [ + "*.login.microsoftonline.com" + ] +} \ No newline at end of file diff --git a/package/manifest.prod.json b/package/manifest.prod.json new file mode 100644 index 0000000..20cef2d --- /dev/null +++ b/package/manifest.prod.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://statics.teams.microsoft.com/sdk/v0.2/manifest/MicrosoftTeams.schema.json", + "manifestVersion": "0.2", + "id": "com.example.microsoftteamstabs.todoprod", + "version": "0.2", + "name": "ToDo", + "developer": { + "name": "Example company", + "websiteUrl": "http://www.example.com", + "privacyUrl": "http://www.example.com/privacy", + "termsOfUseUrl": "http://www.example.com/termsofuse" + }, + "description": { + "short": "Create and manange your Outlook tasks.", + "full": "Create and manange your Outlook tasks. Sign in using your Office 365 work/school account, and click Save." + }, + "icons": { + "44": "todo44.png", + "88": "todo88.png" + }, + "accentColor": "#004578", + "configUrl": "https://teams-todo-sample.azurewebsites.net/config.html", + "canUpdateConfig": true, + "needsIdentity": true, + "validDomains": [ + "*.login.microsoftonline.com" + ] +} \ No newline at end of file diff --git a/package/todo.dev.zip b/package/todo.dev.zip new file mode 100644 index 0000000..953cada Binary files /dev/null and b/package/todo.dev.zip differ diff --git a/package/todo.prod.zip b/package/todo.prod.zip new file mode 100644 index 0000000..dfc46ec Binary files /dev/null and b/package/todo.prod.zip differ diff --git a/package/todo44.png b/package/todo44.png new file mode 100644 index 0000000..46ac264 Binary files /dev/null and b/package/todo44.png differ diff --git a/package/todo88.png b/package/todo88.png new file mode 100644 index 0000000..3e6a79b Binary files /dev/null and b/package/todo88.png differ diff --git a/src/MicrosoftTeams.d.ts b/src/MicrosoftTeams.d.ts new file mode 100644 index 0000000..b3b37fd --- /dev/null +++ b/src/MicrosoftTeams.d.ts @@ -0,0 +1,148 @@ +declare namespace microsoftTeams +{ + // Initializes the library. This must be called before any other API calls. + // The caller should only call this once the frame is loaded successfully. + function initialize(): void; + + // Retrieves the current context the frame is running in. + function getContext(callback: (context: Context) => void): void; + + // Registers a handler for when the user changes their theme. + // Only one handler may be registered at a time. Subsequent registrations will override the first. + function registerOnThemeChangeHandler(handler: (theme: string) => void): void; + + // Navigates the frame to a new cross-domain URL. The domain of this URL must match at least one of the + // valid domains specified in the tab manifest; otherwise, an exception will be thrown. This API only + // needs to be used when navigating the frame to a URL in a different domain than the current one in + // a way that keeps the SkypeTeams app informed of the change and allows the API to continue working. + function navigateCrossDomain(url: string): void; + + // Namespace to interact with the settings view-specific API. + // This object is only usable on the settings frame. + namespace settings + { + // Sets the validity state for the settings. + // The inital value is false so the user will not be able to save the settings until this is called with true. + function setValidityState(validityState: boolean): void; + + // Gets the settings for the current instance. + function getSettings(callback: (settings: Settings) => void): void; + + // Sets the settings for the current instance. + // Note that this is an asynchronous operation so there are no guarentees as to when calls + // to getSettings will reflect the changed state. + function setSettings(settings: Settings): void; + + // Registers a handler for when the user attempts to save the settings. This handler should be used + // to create or update the underlying resource powering the content. + // The object passed to the handler must be used to notify whether to proceed with the save. + // Only one handler may be registered at a time. Subsequent registrations will override the first. + function registerOnSaveHandler(handler: (evt: SaveEvent) => void): void; + + // Registers a handler for when the user attempts to remove the content. This handler should be used + // to remove the underlying resource powering the content. + // The object passed to the handler must be used to notify whether to proceed with the remove + // Only one handler may be registered at a time. Subsequent registrations will override the first. + function registerOnRemoveHandler(handler: (evt: RemoveEvent) => void): void; + + interface Settings + { + // A suggested display name for the new content. + // In the settings for an existing instance being updated, this call has no effect. + suggestedDisplayName?: string; + + // Sets the url to use for the content of this instance. + contentUrl: string; + + // Sets the remove URL for the remove config experience + removeUrl?: string; + + // Sets the url to use for the external link to view the underlying resource in a browser. + websiteUrl?: string; + + // The custom settings for this content instance. + // The developer may use this for generic storage specific to this instance, + // for example a JSON blob describing the previously selected options used to pre-populate the UI. + // The string must be less than 1kb. + customSettings?: string; + } + + interface SaveEvent + { + // Notifies that the underlying resource has been created and the settings may be saved. + notifySuccess(): void; + + // Notifies that the underlying resource creation failed and that the settings may not be saved. + notifyFailure(reason?: string): void; + } + + interface RemoveEvent + { + // Notifies that the underlying resource has been removed and the content may be removed. + notifySuccess(): void; + + // Notifies that the underlying resource removal failed and that the content may not be removed. + notifyFailure(reason?: string): void; + } + } + + namespace authentication + { + // Initiates an authentication request which pops up a new windows with the specified settings. + function authenticate(authenticateParameters: AuthenticateParameters): void; + + // Notifies the frame that initiated this authentication request that the request was successful. + // This function is only usable on the authentication window. + // This call causes the authentication window to be closed. + function notifySuccess(result?: string): void; + + // Notifies the frame that initiated this authentication request that the request failed. + // This function is only usable on the authentication window. + // This call causes the authentication window to be closed. + function notifyFailure(reason?: string): void; + + interface AuthenticateParameters + { + // The url for the authentication popup + url: string, + + // The preferred width for the popup. Note that this value may be ignored if outside the acceptable bounds. + width?: number, + + // The preferred height for the popup. Note that this value may be ignored if outside the acceptable bounds. + height?: number, + + // A function which is called if the authentication succeeds with the result returned from the authentication popup. + successCallback?: (result?: string) => void, + + // A function which is called if the authentication fails with the reason for the failure returned from the authentication popup. + failureCallback?: (reason?: string) => void + } + } + + interface Context + { + // The O365 group id for the team with which the content is associated. + // This field is only available when needsIdentity is set in the manifest. + groupId?: string; + + // The current locale that the user has configured for the app formatted as + // languageId-countryId (e.g. en-us). + locale: string; + + // The current user's upn. + // As a malicious party can host content in a malicious browser, this value should only + // be used as a hint as to who the user is and never as proof of identity. + // This field is only available when needsIdentity is set in the manifest. + upn?: string; + + // The current user's AAD tenant id. + // As a malicious party can host content in a malicious browser, this value should only + // be used as a hint as to who the user is and never as proof of identity. + // This field is only available when needsIdentity is set in the manifest. + tid?: string; + + // The current UI theme the user is using. + theme?: string; + } +} diff --git a/src/app.css b/src/app.css index c85272d..48d9f16 100644 --- a/src/app.css +++ b/src/app.css @@ -2,6 +2,35 @@ body { background-color: #004578; } +body.config { + background-color: #FFFFFF; + margin: 0; + padding: 0; +} + +.config p { + padding: 0; + margin: 0; +} + +.config .ms-Persona { + margin-top: 30px; +} + +.config .ms-Persona-details p:first-child { + margin-bottom: 15px; + opacity: 0.6; +} + +.config .ms-Persona-details p:last-child { + margin-top: 8px; + opacity: 0.6; +} + +.config .ms-Button { + margin-top: 20px; +} + .todoapp h1 { color: #c7e0f4; } diff --git a/src/app.tsx b/src/app.tsx index 61a3eee..5c289eb 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { TodoApp } from './todoApp'; import { TodoModel } from './core'; +import { Authenticator } from '@microsoft/office-js-helpers'; var model = new TodoModel('react-todos'); @@ -12,5 +13,15 @@ function render() { ); } -model.subscribe(render); -render(); \ No newline at end of file +/** initialize throws if called more than once and hence is wrapped in a try-catch to perform a safe initialization. */ + +try { + microsoftTeams.initialize(); +} +catch (e) { +} + +if (!Authenticator.isAuthDialog(true /* Use Microsoft Teams Dialog */)) { + model.subscribe(render); + render(); +} diff --git a/src/config.tsx b/src/config.tsx new file mode 100644 index 0000000..bf158b2 --- /dev/null +++ b/src/config.tsx @@ -0,0 +1,97 @@ +import * as React from 'react'; +import { render } from 'react-dom'; +import { Utilities } from '@microsoft/office-js-helpers'; + +interface IConfigState { + groupId: string; + upn: string; +} + +class Config extends React.Component { + state: IConfigState; + + constructor(props: any) { + super(props); + if (!microsoftTeams) return; + + try { + /* Initialize the Teams library before any other SDK calls. + * Initialize throws if called more than once and hence is wrapped in a try-catch to perform a safe initialization. + */ + + microsoftTeams.initialize(); + } + catch (e) { + } + finally { + this.state = { + groupId: null, + upn: null + }; + + /** Pass the Context interface to the initialize function below */ + microsoftTeams.getContext(context => this.initialize(context as any)); + } + } + + render() { + let output; + + if (this.state) { + output = ( +
+
+
+

Authorized as

+

{this.state.upn}

+

{this.state.groupId}

+
+
+
+ ); + } + else { + output = ( +
+
+
+

Welcome Admin

+

Authorizing...

+
+
+
+ ) + } + + return ( +
{output}
+ ) + } + + /** + * This will save off the Context and registers the Save Handler, which will be called when the user attempts to Save. This handler will be used to save the Settings for the Tab content. + * @param {object} groupId - The O365 group id for the team. + * @param {object} upn - The current user's upn (user principal name). + */ + initialize({ groupId, upn}) { + this.setState({ groupId, upn }); + console.log(this.state); + /** Enable the Save button */ + microsoftTeams.settings.setValidityState(true); + /** Register the save handler */ + microsoftTeams.settings.registerOnSaveHandler(saveEvent => { + /** Store Tab content settings */ + microsoftTeams.settings.setSettings({ + contentUrl: `${location.origin}/index.html`, + suggestedDisplayName: "My Tasks", + websiteUrl: `${location.origin}/index.html` + }); + saveEvent.notifySuccess(); + }); + } +} + +render( + , + document.querySelector('.configApp') +) \ No newline at end of file diff --git a/src/services/outlook.tasks.ts b/src/services/outlook.tasks.ts index 9a3cef5..634ef82 100644 --- a/src/services/outlook.tasks.ts +++ b/src/services/outlook.tasks.ts @@ -80,7 +80,7 @@ export class OutlookTasks implements ITodoService { * the OfficeHelpers library. */ try { - this._token = await this.authenticator.authenticate('Microsoft', false); + this._token = await this.authenticator.authenticate('Microsoft', false, true /* Use Microsoft Teams Dialog */); return this._token; } catch (error) {