Partager via


Prise en main des appels de la bibliothèque d’interface utilisateur Azure Communication Services vers les applications vocales Teams

Ce projet vise à guider les développeurs pour lancer un appel du SDK Web d’appel Azure Communication Services vers la file d’attente d’appels et le standard automatique Teams à l’aide de la bibliothèque d’interface utilisateur Azure Communication.

En fonction de vos besoins, vous devrez peut-être offrir à vos clients un moyen simple de vous contacter sans configuration complexe.

L’appel vers la file d’attente et le standard automatique Teams est un concept simple mais efficace qui facilite l’interaction instantanée avec le support client, un conseiller financier et d’autres équipes orientées client. L’objectif de ce tutoriel est de vous aider à initier des interactions avec vos clients lorsqu’ils cliquent sur un bouton sur le Web.

Si vous souhaitez l’essayer, vous pouvez télécharger le code à partir de GitHub.

Suivez ce tutoriel pour :

  • Vous permettre de contrôler l’expérience audio et vidéo de vos clients en fonction de votre scénario client
  • Apprenez à créer un widget pour démarrer des appels sur votre application web à l’aide de la bibliothèque d’interface utilisateur.

Page d’accueil de l’exemple d’application de widget d’appel

Prérequis

Ces étapes sont nécessaires pour suivre ce tutoriel. Contactez votre administrateur Teams pour les deux derniers éléments pour vous assurer que vous êtes correctement configuré.

Vérification de Node et de Visual Studio Code

Vous pouvez vérifier que Node a été installé correctement avec cette commande.

node -v

La sortie vous indique la version que vous avez, elle échoue si Node n’a pas été installé et ajouté à votre PATH. Comme avec Node, vous pouvez vérifier si VS Code a été installé avec cette commande.

code --version

Comme avec Node, cette commande échoue en cas de problème lors de l’installation de VS Code sur votre ordinateur.

Mise en route

Ce tutoriel comporte 7 étapes et à la fin de l’application sera en mesure d’appeler une application vocale Teams. Les étapes à suivre sont les suivantes :

  1. Configuration du projet
  2. Obtenir vos dépendances
  3. Configuration initiale de l’application
  4. Créer le widget
  5. Styliser le widget
  6. Configurer les valeurs d’identité
  7. Exécuter l’application

1. Configuration du projet

Utilisez cette étape uniquement si vous créez une application.

Pour configurer l’application react, nous utilisons l’outil en ligne de commande create-react-app. Cet outil crée une application TypeScript avec React facile à exécuter.

Pour vous assurer que Node est installé sur votre ordinateur, exécutez cette commande dans PowerShell ou le terminal pour afficher votre version de Node :

node -v

Si vous n’avez pas create-react-app installé sur votre ordinateur, exécutez la commande suivante pour l’installer en tant que commande globale :

npm install -g create-react-app

Une fois cette commande installée, exécutez la commande suivante pour créer une nouvelle application de réaction dans laquelle créer l'exemple :

# Create an Azure Communication Services App powered by React.
npx create-react-app ui-library-calling-widget-app --template typescript

# Change to the directory of the newly created App.
cd ui-library-calling-widget-app

Une fois ces commandes terminées, vous souhaitez ouvrir le projet créé dans VS Code. Vous pouvez ouvrir le projet avec la commande suivante.

code .

2. Obtenir vos dépendances

Ensuite, vous devez mettre à jour le tableau de dépendances dans le package.json pour inclure certains packages d'Azure Communication Services pour que l'expérience de widget que nous allons créer fonctionne :

"@azure/communication-calling": "^1.23.1",
"@azure/communication-chat": "^1.4.0",
"@azure/communication-react": "^1.15.0",
"@azure/communication-calling-effects": "1.0.1",
"@azure/communication-common": "2.3.0",
"@fluentui/react-icons": "~2.0.203",
"@fluentui/react": "~8.98.3",

Pour installer les packages nécessaires, exécutez la commande suivante du gestionnaire de package Node.

npm install

Après avoir installé ces packages, vous devez commencer à écrire le code qui génère l’application. Dans ce tutoriel, nous modifions les fichiers dans le répertoire src.

3. Configuration initiale de l’application

Pour commencer, nous remplaçons le contenu App.tsx fourni par une page principale qui va :

  • Stocker toutes les informations Azure Communication dont nous avons besoin pour créer un CallAdapter pour alimenter notre expérience d’appel
  • Affichez notre widget exposé à l’utilisateur final.

Votre fichier App.tsx doit se présenter ainsi :

src/App.tsx

import "./App.css";
import {
  CommunicationIdentifier,
  MicrosoftTeamsAppIdentifier,
} from "@azure/communication-common";
import {
  Spinner,
  Stack,
  initializeIcons,
  registerIcons,
  Text,
} from "@fluentui/react";
import { CallAdd20Regular, Dismiss20Regular } from "@fluentui/react-icons";
import logo from "./logo.svg";

import { CallingWidgetComponent } from "./components/CallingWidgetComponent";

registerIcons({
  icons: { dismiss: <Dismiss20Regular />, callAdd: <CallAdd20Regular /> },
});
initializeIcons();
function App() {
  /**
   * Token for local user.
   */
  const token = "<Enter your ACS Token here>";

  /**
   * User identifier for local user.
   */
  const userId: CommunicationIdentifier = {
    communicationUserId: "Enter your ACS Id here",
  };

  /**
   * Enter your Teams voice app identifier from the Teams admin center here
   */
  const teamsAppIdentifier: MicrosoftTeamsAppIdentifier = {
    teamsAppId: "<Enter your Teams Voice app id here>",
    cloud: "public",
  };

  const widgetParams = {
    userId,
    token,
    teamsAppIdentifier,
  };

  if (!token || !userId || !teamsAppIdentifier) {
    return (
      <Stack verticalAlign="center" style={{ height: "100%", width: "100%" }}>
        <Spinner
          label={"Getting user credentials from server"}
          ariaLive="assertive"
          labelPosition="top"
        />
      </Stack>
    );
  }

  return (
    <Stack
      style={{ height: "100%", width: "100%", padding: "3rem" }}
      tokens={{ childrenGap: "1.5rem" }}
    >
      <Stack tokens={{ childrenGap: "1rem" }} style={{ margin: "auto" }}>
        <Stack
          style={{ padding: "3rem" }}
          horizontal
          tokens={{ childrenGap: "2rem" }}
        >
          <Text style={{ marginTop: "auto" }} variant="xLarge">
            Welcome to a Calling Widget sample
          </Text>
          <img
            style={{ width: "7rem", height: "auto" }}
            src={logo}
            alt="logo"
          />
        </Stack>

        <Text>
          Welcome to a Calling Widget sample for the Azure Communication
          Services UI Library. Sample has the ability to connect you through
          Teams voice apps to a agent to help you.
        </Text>
        <Text>
          As a user all you need to do is click the widget below, enter your
          display name for the call - this will act as your caller id, and
          action the <b>start call</b> button.
        </Text>
      </Stack>
      <Stack
        horizontal
        tokens={{ childrenGap: "1.5rem" }}
        style={{ overflow: "hidden", margin: "auto" }}
      >
        <CallingWidgetComponent
          widgetAdapterArgs={widgetParams}
          onRenderLogo={() => {
            return (
              <img
                style={{ height: "4rem", width: "4rem", margin: "auto" }}
                src={logo}
                alt="logo"
              />
            );
          }}
        />
      </Stack>
    </Stack>
  );
}

export default App;

Dans cet extrait de code, nous inscrivons deux nouvelles icônes <Dismiss20Regular/> et <CallAdd20Regular>. Ces nouvelles icônes sont utilisées à l’intérieur du composant de widget que nous allons créer dans la section suivante.

4 Créer le widget

Maintenant, nous devons créer un widget qui peut s’afficher dans trois modes différents :

  • En attente : cet état de widget correspond à l’état du composant avant et après l’exécution d’un appel
  • Configuration : cet état correspond au moment où le widget demande des informations à l’utilisateur, comme son nom.
  • Dans un appel : le widget est remplacé ici par le composite d’appel de la bibliothèque d’interface utilisateur. Ce mode widget est utilisé lorsque l’utilisateur appelle l’application vocale ou parle avec un agent.

Créons un dossier nommé src/components. Dans ce dossier, créez un nouveau fichier appelé CallingWidgetComponent.tsx. Ce fichier doit se présenter comme l’extrait de code suivant :

CallingWidgetComponent.tsx

import {
  IconButton,
  PrimaryButton,
  Stack,
  TextField,
  useTheme,
  Checkbox,
  Icon,
  Spinner,
} from "@fluentui/react";
import React, { useEffect, useRef, useState } from "react";
import {
  callingWidgetSetupContainerStyles,
  checkboxStyles,
  startCallButtonStyles,
  callingWidgetContainerStyles,
  callIconStyles,
  logoContainerStyles,
  collapseButtonStyles,
} from "../styles/CallingWidgetComponent.styles";

import {
  AzureCommunicationTokenCredential,
  CommunicationUserIdentifier,
  MicrosoftTeamsAppIdentifier,
} from "@azure/communication-common";
import {
  CallAdapter,
  CallAdapterState,
  CallComposite,
  CommonCallAdapterOptions,
  StartCallIdentifier,
  createAzureCommunicationCallAdapter,
} from "@azure/communication-react";
// lets add to our react imports as well
import { useMemo } from "react";

import { callingWidgetInCallContainerStyles } from "../styles/CallingWidgetComponent.styles";

/**
 * Properties needed for our widget to start a call.
 */
export type WidgetAdapterArgs = {
  token: string;
  userId: CommunicationUserIdentifier;
  teamsAppIdentifier: MicrosoftTeamsAppIdentifier;
};

export interface CallingWidgetComponentProps {
  /**
   *  arguments for creating an AzureCommunicationCallAdapter for your Calling experience
   */
  widgetAdapterArgs: WidgetAdapterArgs;
  /**
   * Custom render function for displaying logo.
   * @returns
   */
  onRenderLogo?: () => JSX.Element;
}

/**
 * Widget for Calling Widget
 * @param props
 */
export const CallingWidgetComponent = (
  props: CallingWidgetComponentProps
): JSX.Element => {
  const { onRenderLogo, widgetAdapterArgs } = props;

  const [widgetState, setWidgetState] = useState<"new" | "setup" | "inCall">(
    "new"
  );
  const [displayName, setDisplayName] = useState<string>();
  const [consentToData, setConsentToData] = useState<boolean>(false);
  const [useLocalVideo, setUseLocalVideo] = useState<boolean>(false);
  const [adapter, setAdapter] = useState<CallAdapter>();

  const callIdRef = useRef<string>();

  const theme = useTheme();

  // add this before the React template
  const credential = useMemo(() => {
    try {
      return new AzureCommunicationTokenCredential(widgetAdapterArgs.token);
    } catch {
      console.error("Failed to construct token credential");
      return undefined;
    }
  }, [widgetAdapterArgs.token]);

  const adapterOptions: CommonCallAdapterOptions = useMemo(
    () => ({
      callingSounds: {
        callEnded: { url: "/sounds/callEnded.mp3" },
        callRinging: { url: "/sounds/callRinging.mp3" },
        callBusy: { url: "/sounds/callBusy.mp3" },
      },
    }),
    []
  );

  const callAdapterArgs = useMemo(() => {
    return {
      userId: widgetAdapterArgs.userId,
      credential: credential,
      targetCallees: [
        widgetAdapterArgs.teamsAppIdentifier,
      ] as StartCallIdentifier[],
      displayName: displayName,
      options: adapterOptions,
    };
  }, [
    widgetAdapterArgs.userId,
    widgetAdapterArgs.teamsAppIdentifier.teamsAppId,
    credential,
    displayName,
  ]);

  useEffect(() => {
    if (adapter) {
      adapter.on("callEnded", () => {
        /**
         * We only want to reset the widget state if the call that ended is the same as the current call.
         */
        if (
          adapter.getState().acceptedTransferCallState &&
          adapter.getState().acceptedTransferCallState?.id !== callIdRef.current
        ) {
          return;
        }
        setDisplayName(undefined);
        setWidgetState("new");
        setConsentToData(false);
        setAdapter(undefined);
        adapter.dispose();
      });

      adapter.on("transferAccepted", (e) => {
        console.log("transferAccepted", e);
      });

      adapter.onStateChange((state: CallAdapterState) => {
        if (state?.call?.id && callIdRef.current !== state?.call?.id) {
          callIdRef.current = state?.call?.id;
          console.log(`Call Id: ${callIdRef.current}`);
        }
      });
    }
  }, [adapter]);

  /** widget template for when widget is open, put any fields here for user information desired */
  if (widgetState === "setup") {
    return (
      <Stack
        styles={callingWidgetSetupContainerStyles(theme)}
        tokens={{ childrenGap: "1rem" }}
      >
        <IconButton
          styles={collapseButtonStyles}
          iconProps={{ iconName: "Dismiss" }}
          onClick={() => {
            setDisplayName(undefined);
            setConsentToData(false);
            setUseLocalVideo(false);
            setWidgetState("new");
          }}
        />
        <Stack tokens={{ childrenGap: "1rem" }} styles={logoContainerStyles}>
          <Stack style={{ transform: "scale(1.8)" }}>
            {onRenderLogo && onRenderLogo()}
          </Stack>
        </Stack>
        <TextField
          label={"Name"}
          required={true}
          placeholder={"Enter your name"}
          onChange={(_, newValue) => {
            setDisplayName(newValue);
          }}
        />
        <Checkbox
          styles={checkboxStyles(theme)}
          label={
            "Use video - Checking this box will enable camera controls and screen sharing"
          }
          onChange={(_, checked?: boolean | undefined) => {
            setUseLocalVideo(!!checked);
            setUseLocalVideo(true);
          }}
        ></Checkbox>
        <Checkbox
          required={true}
          styles={checkboxStyles(theme)}
          disabled={displayName === undefined}
          label={
            "By checking this box, you are consenting that we will collect data from the call for customer support reasons"
          }
          onChange={async (_, checked?: boolean | undefined) => {
            setConsentToData(!!checked);
            if (callAdapterArgs && callAdapterArgs.credential) {
              setAdapter(
                await createAzureCommunicationCallAdapter({
                  displayName: displayName ?? "",
                  userId: callAdapterArgs.userId,
                  credential: callAdapterArgs.credential,
                  targetCallees: callAdapterArgs.targetCallees,
                  options: callAdapterArgs.options,
                })
              );
            }
          }}
        ></Checkbox>
        <PrimaryButton
          styles={startCallButtonStyles(theme)}
          onClick={() => {
            if (displayName && consentToData && adapter) {
              setWidgetState("inCall");
              adapter?.startCall(callAdapterArgs.targetCallees, {
                audioOptions: { muted: false },
              });
            }
          }}
        >
          {!consentToData && `Enter your name`}
          {consentToData && !adapter && (
            <Spinner ariaLive="assertive" labelPosition="top" />
          )}
          {consentToData && adapter && `StartCall`}
        </PrimaryButton>
      </Stack>
    );
  }

  if (widgetState === "inCall" && adapter) {
    return (
      <Stack styles={callingWidgetInCallContainerStyles(theme)}>
        <CallComposite
          adapter={adapter}
          options={{
            callControls: {
              cameraButton: useLocalVideo,
              screenShareButton: useLocalVideo,
              moreButton: false,
              peopleButton: false,
              displayType: "compact",
            },
            localVideoTile: !useLocalVideo ? false : { position: "floating" },
          }}
        />
      </Stack>
    );
  }

  return (
    <Stack
      horizontalAlign="center"
      verticalAlign="center"
      styles={callingWidgetContainerStyles(theme)}
      onClick={() => {
        setWidgetState("setup");
      }}
    >
      <Stack
        horizontalAlign="center"
        verticalAlign="center"
        style={{
          height: "4rem",
          width: "4rem",
          borderRadius: "50%",
          background: theme.palette.themePrimary,
        }}
      >
        <Icon iconName="callAdd" styles={callIconStyles(theme)} />
      </Stack>
    </Stack>
  );
};

Dans le CallAdapterOptions, nous voyons certains fichiers son référencés, ces fichiers doivent utiliser la fonctionnalité Sons appelants dans le CallComposite. Si vous souhaitez utiliser les sons, consultez le code terminé pour télécharger les fichiers son.

5. Styliser le widget

Nous devons écrire certains styles pour nous assurer que l’apparence du widget est appropriée et qu’il peut contenir notre composite d’appel. Ces styles doivent déjà être utilisés dans le widget si vous copiez l’extrait de code que nous avons ajouté au fichier CallingWidgetComponent.tsx.

Créons un nouveau dossier appelé src/styles. Dans ce dossier, créez un fichier appelé CallingWidgetComponent.styles.ts. Le fichier doit se présenter comme l’extrait de code suivant :

import {
  IButtonStyles,
  ICheckboxStyles,
  IIconStyles,
  IStackStyles,
  Theme,
} from "@fluentui/react";

export const checkboxStyles = (theme: Theme): ICheckboxStyles => {
  return {
    label: {
      color: theme.palette.neutralPrimary,
    },
  };
};

export const callingWidgetContainerStyles = (theme: Theme): IStackStyles => {
  return {
    root: {
      width: "5rem",
      height: "5rem",
      padding: "0.5rem",
      boxShadow: theme.effects.elevation16,
      borderRadius: "50%",
      bottom: "1rem",
      right: "1rem",
      position: "absolute",
      overflow: "hidden",
      cursor: "pointer",
      ":hover": {
        boxShadow: theme.effects.elevation64,
      },
    },
  };
};

export const callingWidgetSetupContainerStyles = (
  theme: Theme
): IStackStyles => {
  return {
    root: {
      width: "18rem",
      minHeight: "20rem",
      maxHeight: "25rem",
      padding: "0.5rem",
      boxShadow: theme.effects.elevation16,
      borderRadius: theme.effects.roundedCorner6,
      bottom: 0,
      right: "1rem",
      position: "absolute",
      overflow: "hidden",
      cursor: "pointer",
      background: theme.palette.white,
    },
  };
};

export const callIconStyles = (theme: Theme): IIconStyles => {
  return {
    root: {
      paddingTop: "0.2rem",
      color: theme.palette.white,
      transform: "scale(1.6)",
    },
  };
};

export const startCallButtonStyles = (theme: Theme): IButtonStyles => {
  return {
    root: {
      background: theme.palette.themePrimary,
      borderRadius: theme.effects.roundedCorner6,
      borderColor: theme.palette.themePrimary,
    },
    textContainer: {
      color: theme.palette.white,
    },
  };
};

export const logoContainerStyles: IStackStyles = {
  root: {
    margin: "auto",
    padding: "0.2rem",
    height: "5rem",
    width: "10rem",
    zIndex: 0,
  },
};

export const collapseButtonStyles: IButtonStyles = {
  root: {
    position: "absolute",
    top: "0.2rem",
    right: "0.2rem",
    zIndex: 1,
  },
};

export const callingWidgetInCallContainerStyles = (
  theme: Theme
): IStackStyles => {
  return {
    root: {
      width: "35rem",
      height: "25rem",
      padding: "0.5rem",
      boxShadow: theme.effects.elevation16,
      borderRadius: theme.effects.roundedCorner6,
      bottom: 0,
      right: "1rem",
      position: "absolute",
      overflow: "hidden",
      cursor: "pointer",
      background: theme.semanticColors.bodyBackground,
    },
  };
};

6. Configurer les valeurs d’identité

Avant d’exécuter l’application, accédez à App.tsx et remplacez les valeurs d’espace réservé par vos identités Azure Communication Services et l’identificateur de compte de ressource pour votre application vocale Teams. Voici les valeurs d’entrée des token, des userId et des teamsAppIdentifier.

./src/App.tsx

/**
 * Token for local user.
 */
const token = "<Enter your ACS Token here>";

/**
 * User identifier for local user.
 */
const userId: CommunicationIdentifier = {
  communicationUserId: "Enter your ACS Id here",
};

/**
 * Enter your Teams voice app identifier from the Teams admin center here
 */
const teamsAppIdentifier: MicrosoftTeamsAppIdentifier = {
  teamsAppId: "<Enter your Teams Voice app id here>",
  cloud: "public",
};

7. Exécuter l’application

Enfin, nous pouvons exécuter l’application pour effectuer nos appels ! Exécutez les commandes suivantes pour installer nos dépendances et exécuter notre application.

# Install the new dependencies
npm install

# run the React app
npm run start

Une fois que l’application s’exécute, vous pouvez la voir sur http://localhost:3000 dans votre navigateur. Vous devez normalement voir l’écran de démarrage suivant :

Capture d’écran de l’appel d’un exemple de widget de page d’accueil d’application fermé.

Ensuite, lorsque vous actionnez le bouton du widget, vous devriez voir un petit menu :

Capture d’écran de l’appel d’un exemple de widget de page d’accueil d’application ouvert.

Une fois que vous avez renseigné votre nom, cliquez sur Démarrer l’appel et l’appel doit commencer. Le widget doit ressembler à ceci après le démarrage d’un appel :

Capture d’écran de l’exemple de page d’accueil de l’application avec l’expérience d’appel incorporée dans le widget.

Étapes suivantes

Pour plus d’informations sur les applications vocales Teams, consultez notre documentation sur les standard automatiques Teams et les files d’attente d’appels Teams. Vous pouvez également consulter notre tutoriel sur la création d’une expérience similaire avec les offres groupées JavaScript.

Démarrage rapide : joindre votre application d’appels à une file d’attente d’appels Teams

Démarrage rapide : Joindre votre application d’appels à un standard automatique Teams

Démarrage rapide : Prise en main des offres groupées JavaScript de la bibliothèque d’interface utilisateur Azure Communication Services pour appeler vers la file d’attente des appels Teams et le standard automatique