diff --git a/apps/teams-test-app/e2e-test-data/exampleFeature.json b/apps/teams-test-app/e2e-test-data/exampleFeature.json
new file mode 100644
index 0000000000..cbfd1e790c
--- /dev/null
+++ b/apps/teams-test-app/e2e-test-data/exampleFeature.json
@@ -0,0 +1,58 @@
+{
+ "name": "ExampleFeature",
+ "checkIsSupported": {
+ "version": ">2.0.0-beta.3",
+ "toggleId": "exampleFeatureToggle",
+ "capabilityName": "ExampleFeature"
+ },
+ "featureTests": [
+ {
+ "feature": {
+ "id": "exampleFeature",
+ "version": 2
+ },
+ "testCases": [
+ {
+ "title": "Basic Call - Success",
+ "type": "callResponse",
+ "boxSelector": "#box_exampleFeature",
+ "inputValue": {
+ "input": "test input"
+ },
+ "expectedTestAppValue": "test successful - received: test input"
+ },
+ {
+ "title": "Basic Call - Missing Input",
+ "type": "callResponse",
+ "boxSelector": "#box_exampleFeature",
+ "inputValue": {},
+ "expectedTestAppValue": "Error: Error: Input is required"
+ },
+ {
+ "title": "Register and Raise Event",
+ "type": "callResponse",
+ "boxSelector": "#box_exampleFeatureEvent",
+ "inputValue": {
+ "data": "test data"
+ },
+ "expectedTestAppValue": "event received: test data"
+ },
+ {
+ "title": "Raise Direct Event",
+ "type": "callResponse",
+ "boxSelector": "#box_exampleDirectEvent",
+ "expectedTestAppValue": "Event raised"
+ },
+ {
+ "title": "Regular Test",
+ "type": "callResponse",
+ "boxSelector": "#box_regularTest",
+ "inputValue": {
+ "test": "test input"
+ },
+ "expectedTestAppValue": "regular test complete"
+ }
+ ]
+ }
+ ]
+}
diff --git a/apps/teams-test-app/src/components/ExampleFeatureAPIs.tsx b/apps/teams-test-app/src/components/ExampleFeatureAPIs.tsx
new file mode 100644
index 0000000000..c5fca29e5d
--- /dev/null
+++ b/apps/teams-test-app/src/components/ExampleFeatureAPIs.tsx
@@ -0,0 +1,95 @@
+import { exampleFeature } from '@microsoft/teams-js';
+import React from 'react';
+
+import { ApiWithoutInput, ApiWithTextInput } from './utils';
+import { ModuleWrapper } from './utils/ModuleWrapper';
+
+const CheckExampleFeatureCapability = (): React.ReactElement =>
+ ApiWithoutInput({
+ name: 'checkExampleFeatureCapability',
+ title: 'Check Example Feature Capability',
+ onClick: async () => `ExampleFeature module ${exampleFeature.isSupported() ? 'is' : 'is not'} supported`,
+ });
+
+const BasicCall = (): React.ReactElement =>
+ ApiWithTextInput<{ input: string }>({
+ name: 'exampleFeature',
+ title: 'Basic Call',
+ onClick: {
+ validateInput: (input) => {
+ if (!input.input) {
+ throw new Error('Input is required');
+ }
+ },
+ submit: async (input) => {
+ const response = await exampleFeature.basicCall(input);
+ return response.status;
+ },
+ },
+ defaultInput: JSON.stringify({ input: 'test input' }),
+ });
+
+const RegisterAndRaiseEvent = (): React.ReactElement =>
+ ApiWithTextInput<{ data: string }>({
+ name: 'exampleFeatureEvent',
+ title: 'Register and Raise Event',
+ onClick: {
+ validateInput: (input) => {
+ if (!input.data) {
+ throw new Error('Data is required');
+ }
+ },
+ submit: async (input) => {
+ return new Promise((resolve) => {
+ exampleFeature.registerEventHandler((data) => {
+ resolve(`event received: ${data.data}`);
+ });
+ window.dispatchEvent(
+ new CustomEvent('exampleEvent', {
+ detail: { data: input.data },
+ }),
+ );
+ });
+ },
+ },
+ defaultInput: JSON.stringify({ data: 'test data' }),
+ });
+
+const RaiseDirectEvent = (): React.ReactElement =>
+ ApiWithoutInput({
+ name: 'exampleDirectEvent',
+ title: 'Raise Direct Event',
+ onClick: async () => {
+ exampleFeature.raiseEvent('direct event data');
+ return 'Event raised';
+ },
+ });
+
+const RegularTest = (): React.ReactElement =>
+ ApiWithTextInput({
+ name: 'regularTest',
+ title: 'Regular Test',
+ onClick: {
+ validateInput: (input) => {
+ if (!input) {
+ throw new Error('Input is required');
+ }
+ },
+ submit: async () => {
+ return 'regular test complete';
+ },
+ },
+ defaultInput: JSON.stringify({ test: 'test input' }),
+ });
+
+const ExampleFeatureAPIs = (): React.ReactElement => (
+
+
+
+
+
+
+
+);
+
+export default ExampleFeatureAPIs;
diff --git a/apps/teams-test-app/src/pages/TestApp.tsx b/apps/teams-test-app/src/pages/TestApp.tsx
index f9af8507fb..59d5c59f37 100644
--- a/apps/teams-test-app/src/pages/TestApp.tsx
+++ b/apps/teams-test-app/src/pages/TestApp.tsx
@@ -18,6 +18,7 @@ import DialogUpdateAPIs from '../components/DialogUpdateAPIs';
import DialogUrlAPIs from '../components/DialogUrlAPIs';
import DialogUrlBotAPIs from '../components/DialogUrlBotAPIs';
import DialogUrlParentCommunicationAPIs from '../components/DialogUrlParentCommunicationAPIs';
+import ExampleFeatureAPIs from '../components/ExampleFeatureAPIs';
import GeoLocationAPIs from '../components/GeoLocationAPIs';
import HostEntityTabAPIs from '../components/HostEntityTabAPIs';
import Links from '../components/Links';
@@ -96,6 +97,7 @@ export const TestApp: React.FC = () => {
// List of sections dynamically created from React elements
const sections = useMemo(
() => [
+ { name: 'ExampleFeatureAPIs', component: },
{ name: 'AppAPIs', component: },
{ name: 'AppInitializationAPIs', component: },
{ name: 'AppInstallDialogAPIs', component: },
diff --git a/packages/teams-js/src/private/exampleFeature.ts b/packages/teams-js/src/private/exampleFeature.ts
new file mode 100644
index 0000000000..28a160f641
--- /dev/null
+++ b/packages/teams-js/src/private/exampleFeature.ts
@@ -0,0 +1,77 @@
+import { ensureInitialized } from '../internal/internalAPIs';
+import { runtime } from '../public/runtime';
+
+/**
+ * @internal
+ * Limited to Microsoft-internal use
+ */
+export interface ExampleResponse {
+ /**
+ * Status message returned from the call
+ */
+ status: string;
+}
+
+/**
+ * @internal
+ * Limited to Microsoft-internal use
+ */
+export interface ExampleInput {
+ /**
+ * Input string to send with the call
+ */
+ input: string;
+}
+
+/**
+ * @internal
+ * Limited to Microsoft-internal use
+ */
+export interface ExampleEventData {
+ /**
+ * Data payload for the event
+ */
+ data: string;
+}
+
+/**
+ * @internal
+ * Limited to Microsoft-internal use
+ */
+export function isSupported(): boolean {
+ return ensureInitialized(runtime) && runtime.supports.exampleFeature ? true : false;
+}
+
+/**
+ * @internal
+ * Limited to Microsoft-internal use
+ */
+export function basicCall(input: ExampleInput): Promise {
+ ensureInitialized(runtime);
+ if (!input.input) {
+ throw new Error('Input is required');
+ }
+ return new Promise((resolve) => {
+ resolve({ status: `test successful - received: ${input.input}` });
+ });
+}
+
+/**
+ * @internal
+ * Limited to Microsoft-internal use
+ */
+export function registerEventHandler(handler: (data: ExampleEventData) => void): void {
+ ensureInitialized(runtime);
+ window.addEventListener('exampleEvent', ((event: CustomEvent) => {
+ handler(event.detail);
+ }) as EventListener);
+}
+
+/**
+ * @internal
+ * Limited to Microsoft-internal use
+ */
+export function raiseEvent(eventData: string): void {
+ ensureInitialized(runtime);
+ window.dispatchEvent(new CustomEvent('exampleDirectEvent', { detail: eventData }));
+}
diff --git a/packages/teams-js/src/private/index.ts b/packages/teams-js/src/private/index.ts
index f55e503c3e..e71550ea6e 100644
--- a/packages/teams-js/src/private/index.ts
+++ b/packages/teams-js/src/private/index.ts
@@ -39,3 +39,4 @@ export * as teams from './teams/teams';
export * as videoEffectsEx from './videoEffectsEx';
export * as hostEntity from './hostEntity/hostEntity';
export * as store from './store';
+export * as exampleFeature from './exampleFeature';
diff --git a/packages/teams-js/src/public/runtime.ts b/packages/teams-js/src/public/runtime.ts
index 88c183b7ae..d45c17535c 100644
--- a/packages/teams-js/src/public/runtime.ts
+++ b/packages/teams-js/src/public/runtime.ts
@@ -78,6 +78,7 @@ interface IRuntimeV1 extends IBaseRuntime {
readonly sharedFrame?: {};
};
readonly webStorage?: {};
+ readonly exampleFeature?: {};
};
}
@@ -142,6 +143,7 @@ interface IRuntimeV2 extends IBaseRuntime {
readonly sharedFrame?: {};
};
readonly webStorage?: {};
+ readonly exampleFeature?: {};
};
}
@@ -214,6 +216,7 @@ interface IRuntimeV3 extends IBaseRuntime {
readonly image?: {};
};
readonly webStorage?: {};
+ readonly exampleFeature?: {};
};
}
@@ -309,6 +312,7 @@ interface IRuntimeV4 extends IBaseRuntime {
readonly image?: {};
};
readonly webStorage?: {};
+ readonly exampleFeature?: {};
};
}
// Constant used to set the runtime configuration
@@ -365,7 +369,7 @@ export let runtime: Runtime | UninitializedRuntime = _uninitializedRuntime;
* during initialization.
*/
export const versionAndPlatformAgnosticTeamsRuntimeConfig: Runtime = {
- apiVersion: 4,
+ apiVersion: latestRuntimeApiVersion,
isNAAChannelRecommended: false,
hostVersionsInfo: teamsMinAdaptiveCardVersion,
isLegacyTeams: true,
@@ -385,6 +389,7 @@ export const versionAndPlatformAgnosticTeamsRuntimeConfig: Runtime = {
},
update: {},
},
+ exampleFeature: {},
interactive: {},
logs: {},
meetingRoom: {},