Skip to content

Commit

Permalink
Add Open App Store & Notify App Install Completed Capability (#2626)
Browse files Browse the repository at this point in the history
* enable open app store experience and add unit test file

* changefile

* update unit tes file

* update teams-test-app StoreApis

* 1. eliminate some parameters of openStoreExperience & fix by comment
2. add e2e test
3. update unit test

* update isSupported

* otherAppStateChange file change migration @Zhitao

* Update change/@microsoft-teams-js-07829a41-a760-48a1-a793-df938ea61c13.json

Co-authored-by: Trevor Harris <trharris@microsoft.com>

* Update packages/teams-js/src/private/store.ts

Co-authored-by: Trevor Harris <trharris@microsoft.com>

* Update packages/teams-js/src/private/store.ts

Co-authored-by: Trevor Harris <trharris@microsoft.com>

* add unit test for notifyInstall & update error thrown code

* append 'hostSdkVersion' parameter in e2e test data

* Update packages/teams-js/src/private/store.ts

* Update packages/teams-js/src/private/store.ts

Co-authored-by: Trevor Harris <trharris@microsoft.com>

* update e2e test

* update e2e test for notifyAppInstall

* add collectionId parameter & modify related test files

* add new error message for invalid dialog type & update test file

* try catch error in test app

* parse open store params json to hub sdk to eliminate redundant knowledge in hub sdk

* update e2e test file

* rename notifyAppInstall with notifyInstallCompleted

* update name

* parse parameters to hub sdk

* add test cases to store e2e & update notify install title

* update store e2e

---------

Co-authored-by: Hang Yin <hangyin@microsoft.com>
Co-authored-by: Trevor Harris <trharris@microsoft.com>
  • Loading branch information
3 people authored Nov 21, 2024
1 parent 8d2d55b commit b558cec
Show file tree
Hide file tree
Showing 13 changed files with 545 additions and 7 deletions.
13 changes: 9 additions & 4 deletions apps/teams-test-app/e2e-test-data/otherAppStateChange.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,22 @@
"type": "registerAndRaiseEvent",
"boxSelector": "#box_otherAppStateChange_registerInstallHandler",
"eventName": "otherApp.install",
"eventData":
{
"appIds": ["123", "456"]
},
"eventData": {
"appIds": ["123", "456"]
},
"expectedTestAppValue": "received"
},
{
"title": "unregisterAppInstallationHandler - Sends but not processed if app is not approved",
"type": "callResponse",
"boxSelector": "#box_otherAppStateChange_unregisterInstallHandler",
"expectedTestAppValue": "received"
},
{
"title": "notifyInstallCompleted - Success",
"type": "callResponse",
"boxSelector": "#box_otherAppStateChange_notifyInstallCompleted",
"expectedTestAppValue": "notified"
}
]
}
118 changes: 118 additions & 0 deletions apps/teams-test-app/e2e-test-data/store.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
{
"name": "Store",
"platforms": "Web",
"checkIsSupported": {
"domElementName": "checkCapabilityStore"
},
"version": ">2.31.0",
"hostSdkVersion": {
"web": ">4.5.0"
},
"testCases": [
{
"title": "openStoreExperience API Call Full Store - Success",
"type": "callResponse",
"boxSelector": "#box_storeOpen",
"inputValue": {
"dialogType": "fullstore"
},
"expectedAlertValue": "openStoreExperience called with ##JSON_INPUT_VALUE##"
},
{
"title": "openStoreExperience API Call In-Context-Store - Success",
"type": "callResponse",
"boxSelector": "#box_storeOpen",
"inputValue": {
"dialogType": "ics"
},
"expectedAlertValue": "openStoreExperience called with ##JSON_INPUT_VALUE##"
},
{
"title": "openStoreExperience API Call App Detail - Success",
"type": "callResponse",
"boxSelector": "#box_storeOpen",
"inputValue": {
"dialogType": "appdetail",
"appId": "1542629c-01b3-4a6d-8f76-1938b779e48d"
},
"expectedAlertValue": "openStoreExperience called with ##JSON_INPUT_VALUE##"
},
{
"title": "openStoreExperience API Call App Detail With Invalid AppId - Fail",
"type": "callResponse",
"boxSelector": "#box_storeOpen",
"inputValue": {
"dialogType": "appdetail",
"appId": "123"
},
"expectedTestAppValue": "Error: Error: Potential app id (123) is invalid; its length 3 is not within the length limits (4-256)."
},
{
"title": "openStoreExperience API Call App Detail With Empty AppId - Fail",
"type": "callResponse",
"boxSelector": "#box_storeOpen",
"inputValue": {
"dialogType": "appdetail",
"appId": ""
},
"expectedTestAppValue": "Error: Error: Potential app id () is invalid; its length 0 is not within the length limits (4-256)."
},
{
"title": "openStoreExperience API Call App Detail Without AppId - Fail",
"type": "callResponse",
"boxSelector": "#box_storeOpen",
"inputValue": {
"dialogType": "appdetail"
},
"expectedTestAppValue": "Error: Error: No App Id present, but AppId needed to open AppDetail store"
},
{
"title": "openStoreExperience API Call Specific Store - Success",
"type": "callResponse",
"boxSelector": "#box_storeOpen",
"inputValue": {
"dialogType": "specificstore",
"collectionId": "copilotplugins"
},
"expectedAlertValue": "openStoreExperience called with ##JSON_INPUT_VALUE##"
},
{
"title": "openStoreExperience API Call Specific Store With Empty CollectionId - Fail",
"type": "callResponse",
"boxSelector": "#box_storeOpen",
"inputValue": {
"dialogType": "specificstore",
"collectionId": ""
},
"expectedTestAppValue": "Error: Error: No Collection Id present, but CollectionId needed to open a store specific to a collection"
},
{
"title": "openStoreExperience API Call Specific Store Without CollectionId - Fail",
"type": "callResponse",
"boxSelector": "#box_storeOpen",
"inputValue": {
"dialogType": "specificstore"
},
"expectedTestAppValue": "Error: Error: No Collection Id present, but CollectionId needed to open a store specific to a collection"
},
{
"title": "openStoreExperience API Call With Invalid Dialog Type - Fail",
"type": "callResponse",
"boxSelector": "#box_storeOpen",
"inputValue": {
"dialogType": "123"
},
"expectedTestAppValue": "Error: Error: Invalid store dialog type, but type needed to specify store to open"
},
{
"title": "openStoreExperience API Call Without Permission - Fail",
"type": "callResponse",
"boxSelector": "#box_storeOpen",
"inputValue": {
"dialogType": "fullstore"
},
"testUrlParams": [["appDefOverrides", "{\"isFullTrustApp\": false, \"isMicrosoftOwned\": false}"]],
"expectedTestAppValue": "Error: Error: 500, message: App does not have the required permissions for this operation"
}
]
}
13 changes: 12 additions & 1 deletion apps/teams-test-app/src/components/OtherAppStateChangeAPIs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { otherAppStateChange } from '@microsoft/teams-js';
import { AppId, otherAppStateChange } from '@microsoft/teams-js';
import React, { ReactElement } from 'react';

import { ApiWithoutInput } from './utils';
Expand Down Expand Up @@ -39,12 +39,23 @@ const UnregisterAppInstallHandler = (): React.ReactElement =>
},
});

const NotifyInstallCompletedHandler = (): React.ReactElement =>
ApiWithoutInput({
name: 'otherAppStateChange_notifyInstallCompleted',
title: 'Notify Install Completed',
onClick: async () => {
otherAppStateChange.notifyInstallCompleted(new AppId('12345'));
return 'notified';
},
});

const OtherAppStateChangedAPIs = (): ReactElement => (
<>
<ModuleWrapper title="OtherAppStateChanged">
<CheckOtherAppStateChangeCapability />
<RegisterAppInstallHandler />
<UnregisterAppInstallHandler />
<NotifyInstallCompletedHandler />
</ModuleWrapper>
</>
);
Expand Down
61 changes: 61 additions & 0 deletions apps/teams-test-app/src/components/StoreApis.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { AppId, store } from '@microsoft/teams-js';
import { ReactElement } from 'react';
import React from 'react';

import { ApiWithoutInput, ApiWithTextInput } from './utils';
import { ModuleWrapper } from './utils/ModuleWrapper';

const StoreAPIs = (): ReactElement => {
const CheckStoreCapability = (): ReactElement =>
ApiWithoutInput({
name: 'checkCapabilityStore',
title: 'Check Capability Store',
onClick: async () => {
if (store.isSupported()) {
return 'Store module is supported';
} else {
return 'Store module is not supported';
}
},
});

const OpenStore = (): ReactElement =>
ApiWithTextInput<{ dialogType: string; appId?: string; collectionId?: string }>({
name: 'storeOpen',
title: 'Store Open',
onClick: {
validateInput: (input) => {
if (input?.dialogType === undefined) {
throw new Error('store type undefined');
}
},
submit: async (input) => {
const appId = input.appId === undefined ? undefined : new AppId(input.appId);
const openStoreParam = {
dialogType: input.dialogType,
appId: appId,
collectionId: input.collectionId,
};
// eslint-disable-next-line no-useless-catch
try {
await store.openStoreExperience(openStoreParam as store.OpenStoreParams);
return 'store opened';
} catch (e) {
throw e;
}
},
},
defaultInput: JSON.stringify({
dialogType: 'appdetail',
appId: '1542629c-01b3-4a6d-8f76-1938b779e48d',
}),
});
return (
<ModuleWrapper title="Store">
<CheckStoreCapability />
<OpenStore />
</ModuleWrapper>
);
};

export default StoreAPIs;
2 changes: 2 additions & 0 deletions apps/teams-test-app/src/pages/TestApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import SecondaryBrowserAPIs from '../components/SecondaryBrowserAPIs';
import SharingAPIs from '../components/SharingAPIs';
import StageViewAPIs from '../components/StageViewAPIs';
import StageViewSelfAPIs from '../components/StageViewSelfAPIs';
import StoreAPIs from '../components/StoreApis';
import TeamsCoreAPIs from '../components/TeamsCoreAPIs';
import ThirdPartyCloudStorageAPIs from '../components/ThirdPartyCloudStorageAPIs';
import CookieAccessComponent from '../components/ThirdPatryCookies';
Expand Down Expand Up @@ -163,6 +164,7 @@ export const TestApp: React.FC = () => {
{ name: 'VideoAPIs', component: <VideoAPIs /> },
{ name: 'VideoExAPIs', component: <VideoExAPIs /> },
{ name: 'VisualMediaAPIs', component: <VisualMediaAPIs /> },
{ name: 'StoreAPIs', component: <StoreAPIs /> },
],
[],
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Added `store` capability that will enable user to open several types of app store dialogs.. The capability is still awaiting support in one or most host applications. To track availability of this capability across different hosts see https://aka.ms/capmatrix",
"packageName": "@microsoft/teams-js",
"email": "yt6520143@163.com",
"dependentChangeType": "patch"
}
2 changes: 2 additions & 0 deletions packages/teams-js/src/internal/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ export const enum ApiName {
Notifications_ShowNotification = 'notifications.showNotification',
OtherAppStateChange_Install = 'otherApp.install',
OtherAppStateChange_UnregisterInstall = 'otherApp.unregisterInstall',
OtherAppStateChange_NotifyInstallCompleted = 'otherApp.notifyInstallCompleted',
Pages_AppButton_OnClick = 'pages.appButton.onClick',
Pages_AppButton_OnHoverEnter = 'pages.appButton.onHoverEnter',
Pages_AppButton_OnHoverLeave = 'pages.appButton.onHoverLeave',
Expand Down Expand Up @@ -322,6 +323,7 @@ export const enum ApiName {
Sharing_ShareWebContent = 'sharing.shareWebContent',
StageView_Open = 'stageView.open',
StageView_Self_Close = 'stageView.self.close',
Store_Open = 'store.open',
Tasks_StartTask = 'tasks.startTask',
Tasks_SubmitTask = 'tasks.submitTask',
Tasks_UpdateTask = 'tasks.updateTask',
Expand Down
1 change: 1 addition & 0 deletions packages/teams-js/src/private/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ export * as appEntity from './appEntity';
export * as teams from './teams/teams';
export * as videoEffectsEx from './videoEffectsEx';
export * as hostEntity from './hostEntity/hostEntity';
export * as store from './store';
27 changes: 26 additions & 1 deletion packages/teams-js/src/private/otherAppStateChange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
* when another application has been installed.
*/

import { sendMessageToParent } from '../internal/communication';
import { callFunctionInHost, sendMessageToParent } from '../internal/communication';
import { registerHandler, removeHandler } from '../internal/handlers';
import { ensureInitialized } from '../internal/internalAPIs';
import { ApiName, ApiVersionNumber, getApiVersionTag } from '../internal/telemetry';
import { isNullOrUndefined } from '../internal/typeCheckUtilities';
import { AppId } from '../public/appId';
import { ErrorCode } from '../public/interfaces';
import { runtime } from '../public/runtime';

Expand Down Expand Up @@ -112,6 +113,30 @@ export function unregisterAppInstallationHandler(): void {
removeHandler(ApiName.OtherAppStateChange_Install);
}

/**
* @hidden
* @beta
* @internal
* Limited to Microsoft-internal use
*
* This function should be called by the Store App to notify the host that the
* app with the given appId has been installed.
*
* @throws Error if {@link app.initialize} has not successfully completed or if the platform
* does not support the otherAppStateChange capability.
*/
export function notifyInstallCompleted(appId: AppId): Promise<void> {
if (!isSupported()) {
throw new Error(ErrorCode.NOT_SUPPORTED_ON_PLATFORM.toString());
}

return callFunctionInHost(
ApiName.OtherAppStateChange_NotifyInstallCompleted,
[appId.toString()],
getApiVersionTag(otherAppStateChangeTelemetryVersionNumber, ApiName.OtherAppStateChange_NotifyInstallCompleted),
);
}

/**
* Checks if the otherAppStateChange capability is supported by the host
* @returns boolean to represent whether the otherAppStateChange capability is supported
Expand Down
Loading

0 comments on commit b558cec

Please sign in to comment.