SharePoint / sp-dev-docs

SharePoint & Viva Connections Developer Documentation
https://docs.microsoft.com/en-us/sharepoint/dev/
Creative Commons Attribution 4.0 International
1.25k stars 1.01k forks source link

SPFx in Teams tab not rendering until browsed to in spo #9920

Open JonoSuave opened 1 month ago

JonoSuave commented 1 month ago

Target SharePoint environment

SharePoint Online

What SharePoint development model, framework, SDK or API is this about?

💥 SharePoint Framework

Developer environment

macOS

What browser(s) / client(s) have you tested

Additional environment details

Describe the bug / error

I have a custom Teams personal app that has two tabs: one tab surfaces an spfx webpart, while the other a React SPA provisioned and deployed using the Teams Toolkit. After a couple days of not browsing to the custom Teams app, however, the tab that surfaces the SPFx component is stuck on a spinner -- the other tab has no problems. The console gives me the following error: App resource defined in manifest and iframe origin do not match failureCallback @ TeamsLogon.aspx?SPFX...rceLocale=en-us:240

In order to get the spfx tab to render I have to do the following in Chrome and Edge: browse to a SharePoint page with the webpart and then refresh the static tab in Teams

On the Teams Desktop client I added a reference to the SharePoint page in a Teams Channel and after browsing to that Teams channel tab I can then see the spfx component in the custom app's spfx tab

Steps to reproduce

Expected behavior

I shouldn't have to browse to the SPO page with the spfx webpart to then see it in the custom Teams app

Screenshot 2024-09-16 at 12 48 05 PM Screenshot 2024-09-16 at 12 48 25 PM
JonoSuave commented 1 month ago

Here's what my Teams Toolkit manifest.json looks like:

{
  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json",
  "version": "1.26.0",
  "manifestVersion": "1.17",
  "id": "bda4dea9-fa92-4f2c-81a1-8b95a8c70e40",
  "name": {
    "short": "Directory",
    "full": "Full name for Directory"
  },
  "developer": {
    "name": "JourneyTEAM, LLC",
    "websiteUrl": "https://www.journeyteam.com",
    "privacyUrl": "https://www.journeyteam.com",
    "termsOfUseUrl": "https://www.journeyteam.com"
  },
  "description": {
    "short": "Short description of DepartmentDirectory",
    "full": "Full description of DepartmentDirectory"
  },
  "icons": {
    "outline": "outline.png",
    "color": "color.png"
  },
  "accentColor": "#FFFFFF",
  "staticTabs": [
    {
      "entityId": "SPFxEmployeeDirectoryTab",
      "name": "Faculty & Staff",
      "contentUrl": "https://(redacted_domain_name).sharepoint.com/_layouts/15/TeamsLogon.aspx?SPFX=true&dest=/_layouts/15/teamshostedapp.aspx%3Fteams%26personal%26componentId=386dd1b8-71f5-4d5d-a568-b44b211067f0%26forceLocale=en-us",
      "scopes": [
        "personal"
      ],
      "context": [
        "personalTab"
      ]
    },
    {
      "entityId": "DepartmentDirectoryTab",
      "name": "Departments",
      "contentUrl": "https://ashy-hill-0f884b110.5.azurestaticapps.net/index.html#/department-directory",
      "websiteUrl": "https://ashy-hill-0f884b110.5.azurestaticapps.net/index.html#/department-directory",
      "scopes": [
        "personal"
      ]
    },
    {
      "entityId": "about",
      "scopes": [
        "personal"
      ]
    }
  ],
  "validDomains": [
    "(redacted_domain_name).sharepoint.com",
    "ashy-hill-0f884b110.5.azurestaticapps.net",
    "*.login.microsoftonline.com",
    "*.sharepoint.com",
    "*.sharepoint-df.com",
    "spoppe-a.akamaihd.net",
    "spoprod-a.akamaihd.net",
    "ashy-hill-0f884b110.5.azurestaticapps.net*"
  ],
  "webApplicationInfo": {
    "id": "2db8265f-5a18-4d83-a39c-335603046850",
    "resource": "api://ashy-hill-0f884b110.5.azurestaticapps.net/2db8265f-5a18-4d83-a39c-335603046850"
  },
  "authorization": {
    "permissions": {
      "resourceSpecific": []
    }
  }
}
JonoSuave commented 1 month ago

Here's what my spfx teams manifest.json looks like:

{
    "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json",
    "version": "1.2",
    "manifestVersion": "1.17",
    "id": "aa1185ab-38c1-4621-b54d-90e94523e602",
    "developer": {
        "name": "JourneyTEAM, LLC",
        "mpnId": "",
        "websiteUrl": "https://www.journeyteam.com",
        "privacyUrl": "https://www.journeyteam.com",
        "termsOfUseUrl": "https://www.journeyteam.com"
    },
    "name": {
        "short": "Teams Faculty Directory",
        "full": "Teams Faculty Directory"
    },
    "description": {
        "short": "Teams Employee Directory description",
        "full": "Teams Employee Directory description"
    },
    "icons": {
        "outline": "aa1185ab-38c1-4621-b54d-90e94523e602_outline.png",
        "color": "aa1185ab-38c1-4621-b54d-90e94523e602_color.png"
    },
    "accentColor": "#FFFFFF",
    "staticTabs": [
        {
            "entityId": "Belmont Faculty Directory",
            "name": "Faculty Directory",
            "contentUrl": "https://(redacted_domain_name).sharepoint.com/_layouts/15/TeamsLogon.aspx?SPFX=true&dest=/_layouts/15/teamshostedapp.aspx%3Fteams%26personal%26componentId=386dd1b8-71f5-4d5d-a568-b44b211067f0%26forceLocale=en-us",
            "scopes": ["personal"],
            "context": ["personalTab"]
        },
        {
            "entityId": "about",
            "scopes": ["personal"]
        }
    ],
    "validDomains": [
        "*.login.microsoftonline.com",
        "*.sharepoint.com",
        "*.sharepoint-df.com",
        "spoppe-a.akamaihd.net",
        "spoprod-a.akamaihd.net",
        "resourceseng.blob.core.windows.net",
        "msft.spoppe.com"
    ],
    "webApplicationInfo": {
        "id": "00000003-0000-0ff1-ce00-000000000000",
        "resource": ""
    }
}
JonoSuave commented 1 month ago

Here's an example of how I call the two components from two separate webparts:

import * as React from "react";
import { useMemo, useState } from "react";
import styles from "./FacultyStudentTeamsDirectory.module.scss";
import type { IFacultyStudentTeamsDirectoryProps } from "./IFacultyStudentTeamsDirectoryProps";
import EmployeeDirectory from "../../employeeDirectory/components/EmployeeDirectory";
import { IReactTemplateProps } from "@journeyteam/directory-extensions";
import SearchBox from "../../searchBox/components/SearchBox";
import { ISelectedRefiner, ISelectedRefinerValues } from "../../../models/refiner";
import * as SPSearch from "@pnp/sp/search/types";

export declare enum TemplateType {
    adaptiveCard = "adaptive-card",
    react = "react",
    handlebars = "handelbars",
}
export default function FacultyStudentTeamsDirectory(props: IFacultyStudentTeamsDirectoryProps) {
    const {
        isDarkTheme,
        hasTeamsContext,
        initialSearchText,
        employeeCardTemplate,
        studentView,
        propertyPanelOpen,
        focusSections,
        directoryRefiners,
        refiners,
    } = props;

    const [selectedRefiners, setSelectedRefiners] =
        useState<ISelectedRefinerValues[]>(directoryRefiners);
    const [searchBoxRefinersSourceData, setSearchBoxRefinersSourceData] = useState<
        SPSearch.IRefiner[]
    >([]);
    const [searchText, setSearchText] = useState(initialSearchText ? initialSearchText : "");
    const [cardWidth, setCardWidth] = useState(275);

    useMemo(() => {
        const handleResize = () => {
            if (window.innerWidth < 500) {
                setCardWidth(150);
            } else if (window.innerWidth < 800) {
                setCardWidth(200);
            } else {
                setCardWidth(275);
            }
        };

        // Call the handleResize function initially to set the width based on the current viewport size
        handleResize();

        // Set up the event listener for the resize event
        window.addEventListener("resize", handleResize);

        // Clean up the event listener when the component is unmounted
        return () => {
            window.removeEventListener("resize", handleResize);
        };
    }, []);

    const searchBoxRefinersChanged = (searchBoxRefiners: ISelectedRefiner[]) => {
        const mappedSelectedRefiners = refiners.map<ISelectedRefinerValues>((refiner) => {
            const matchedRefiners = searchBoxRefiners.filter(
                (selectedRefiner) => selectedRefiner.Name === refiner.PropertyName
            );
            return {
                PropertyName: refiner.PropertyName,
                SelectedRefiners: matchedRefiners,
            };
        });
        setSelectedRefiners(mappedSelectedRefiners);
    };

    const onSearchChange = (val: string) => {
        setSearchText(val);
    };

    const _refinersChanged = (refiners: SPSearch.IRefiner[]) => {
        if (searchBoxRefinersSourceData.length === 0) {
            setSearchBoxRefinersSourceData(refiners);
        }
        if (searchBoxRefinersSourceData[0]?.Entries.length < 20) {
            setSearchBoxRefinersSourceData(refiners);
        }
    };

    return (
        <section
            className={`${styles.facultyStudentTeamsDirectory} ${hasTeamsContext ? styles.teams : ""}`}>
            <SearchBox
                onSearchChanged={onSearchChange}
                initialSearchText={initialSearchText}
                refinerSourceData={searchBoxRefinersSourceData}
                refiners={props.refiners}
                onSelectedRefinersChanged={searchBoxRefinersChanged}
                searchByFilters={props.searchByFilters}
                searchByLabel={undefined}
                searchByPlaceholder="Search By"
                refinersLabel=""
                refinersPlaceholder="Search Refiners"
                searchLabel={""}
                searchPlaceholder={""}
            />
            <br />
            <br />

            <EmployeeDirectory
                 searchText={`${
                    searchText}*`}
                itemsPerPage={20}
                preFilter={`-"SPS-HideFromAddressLists":1`}
                refiners={selectedRefiners}
                onRefinersChanged={_refinersChanged}
                useLibraryTemplate={false}
                showProfileLink={undefined}
                customStyles={undefined}
                employeeCardTemplate={employeeCardTemplate}
                focusSections={focusSections}
                headerSections={props.headerSections}
                otherSections={[]}
                sourceId={undefined}
                sort={[
                    {
                        Property: "firstName",
                        Direction: 0,
                    },
                ]}
                hiddenRefiner={[]}
                serviceScope={props.serviceScope}
                context={props.context}
                templates={props.templates}
                editMode={true}
                employeeCardWidth={cardWidth}
                propertyPanelOpen={propertyPanelOpen}
                paginationLocation={1}
                toggleDirectReportsExport={false}
                directReportsExportUrl=""
                studentView={studentView}
                isDarkTheme={isDarkTheme}
            />
        </section>
    );
}
JonoSuave commented 1 month ago

Further update...just deploying my spfx solution and syncing to Teams works as a standalone personal app. The personal app that has multiple personal tabs (one being the spfx component and the other being a React app provisioned and deployed in Entra) doesn't work for the spfx tab until I first navigate to the standalone spfx personal app

Screenshot 2024-09-18 at 9 09 25 AM

JonoSuave commented 1 month ago

@VesaJuvonen any update on your guys end by chance?

JonoSuave commented 1 month ago

From what I'm seeing, seems the issue may lie in that the teams app manifest only allows for one set of id and resource to be referenced in the webApplicationInfo. In my case, one tab is leveraging an spfx component (resource would be the teamsitedomain and id the multi-tentant Microsoft Graph app id) and the other a teams app that was provisioned in entra using the Teams Toolkit.