@forge/react

11.9.2-next.0-experimental-919607a11.10.0-next.1
~

Modified (7 files)

Index: package/out/hooks/usePermissions.js
===================================================================
--- package/out/hooks/usePermissions.js
+++ package/out/hooks/usePermissions.js
@@ -2,35 +2,9 @@
 Object.defineProperty(exports, "__esModule", { value: true });
 exports.usePermissions = void 0;
 const react_1 = require("react");
 const bridge_1 = require("@forge/bridge");
-const egress_1 = require("@forge/egress");
 /**
- * https://ecosystem-platform.atlassian.net/browse/DEPLOY-1411
- * Uses @forge/egress for URL matching (same logic as @forge/api)
- */
-/**
- * Helper function to extract URL string from external URL permissions.
- * Matches the implementation in @forge/api for consistency.
- */
-function extractUrlString(url) {
-    if (typeof url === 'string') {
-        return url;
-    }
-    if ('address' in url && url.address) {
-        return url.address;
-    }
-    return url.remote || '';
-}
-/**
- * Resource types that can be loaded externally
- */
-const RESOURCE_TYPES = ['fonts', 'styles', 'frames', 'images', 'media', 'scripts'];
-/**
- * Fetch types for external requests
- */
-const FETCH_TYPES = ['backend', 'client'];
-/**
  * Hook for checking permissions in Forge apps
  *
  * @param requiredPermissions - The permissions required for the component
  * @returns Object containing permission state, loading status, and error information
@@ -68,8 +42,12 @@
 const usePermissions = (requiredPermissions) => {
     const [context, setContext] = (0, react_1.useState)();
     const [isLoading, setIsLoading] = (0, react_1.useState)(true);
     const [error, setError] = (0, react_1.useState)(null);
+    const [permissionResult, setPermissionResult] = (0, react_1.useState)({
+        granted: false,
+        missing: null
+    });
     // Load context on mount
     (0, react_1.useEffect)(() => {
         const loadContext = async () => {
             try {
@@ -86,108 +64,39 @@
             }
         };
         void loadContext();
     }, []);
-    // Permission checking utilities
-    const permissionUtils = (0, react_1.useMemo)(() => {
-        if (!context?.permissions)
-            return null;
-        const { scopes, external = {} } = context.permissions;
-        const scopeArray = Array.isArray(scopes) ? scopes : Object.keys(scopes || {});
-        return {
-            hasScope: (scope) => scopeArray.includes(scope),
-            canFetchFrom: (type, url) => {
-                const fetchUrls = external.fetch?.[type];
-                if (!fetchUrls?.length)
-                    return false;
-                // Extract URLs and create egress filter
-                const allowList = fetchUrls.map(extractUrlString).filter((u) => u.length > 0);
-                if (allowList.length === 0)
-                    return false;
-                const egressFilter = new egress_1.EgressFilteringService(allowList);
-                // Backend: hostname-only matching, Client: CSP validation (includes paths)
-                return type === 'client' ? egressFilter.isValidUrlCSP(url) : egressFilter.isValidUrl(url);
-            },
-            canLoadResource: (type, url) => {
-                const resourceUrls = external[type];
-                if (!resourceUrls?.length)
-                    return false;
-                // Extract URLs and create egress filter
-                const allowList = resourceUrls.map(extractUrlString).filter((u) => u.length > 0);
-                if (allowList.length === 0)
-                    return false;
-                const egressFilter = new egress_1.EgressFilteringService(allowList);
-                // All resources use CSP validation (checks protocol + hostname + paths)
-                return egressFilter.isValidUrlCSP(url);
-            },
-            getScopes: () => scopeArray,
-            getExternalPermissions: () => external,
-            hasAnyPermissions: () => scopeArray.length > 0 || Object.keys(external).length > 0
-        };
-    }, [context?.permissions]);
-    // Check permissions
-    const permissionResult = (0, react_1.useMemo)(() => {
-        if (!requiredPermissions) {
-            return { granted: false, missing: null };
+    // Check permissions using shared utility
+    (0, react_1.useEffect)(() => {
+        // Skip if still loading context
+        if (isLoading) {
+            return;
         }
-        if (!permissionUtils) {
-            // If still loading or there's an error, return null for missing permissions
-            if (isLoading || error) {
-                return { granted: false, missing: null };
+        const checkPerms = async () => {
+            if (!requiredPermissions) {
+                setPermissionResult({ granted: false, missing: null });
+                return;
             }
-            throw new Error('This feature is not available yet');
-        }
-        const missing = {};
-        let hasAllRequiredPermissions = true;
-        // Check scopes
-        if (requiredPermissions.scopes?.length) {
-            const missingScopes = requiredPermissions.scopes.filter((scope) => !permissionUtils.hasScope(scope));
-            if (missingScopes.length > 0) {
-                missing.scopes = missingScopes;
-                hasAllRequiredPermissions = false;
-            }
-        }
-        // Check external permissions
-        if (requiredPermissions.external) {
-            const missingExternal = {};
-            // Check fetch permissions
-            if (requiredPermissions.external.fetch) {
-                const missingFetch = {};
-                FETCH_TYPES.forEach((type) => {
-                    const requiredUrls = requiredPermissions.external?.fetch?.[type];
-                    if (requiredUrls?.length) {
-                        const missingUrls = requiredUrls.filter((url) => !permissionUtils.canFetchFrom(type, url));
-                        if (missingUrls.length > 0) {
-                            missingFetch[type] = missingUrls;
-                            hasAllRequiredPermissions = false;
-                        }
-                    }
-                });
-                if (Object.keys(missingFetch).length > 0) {
-                    missingExternal.fetch = missingFetch;
+            if (!context?.permissions) {
+                // If context loaded but has no permissions, set error
+                if (context !== undefined) {
+                    setError(new Error('This feature is not available yet'));
+                    setPermissionResult({ granted: false, missing: null });
                 }
+                return;
             }
-            // Check resource permissions
-            RESOURCE_TYPES.forEach((type) => {
-                const requiredUrls = requiredPermissions.external?.[type];
-                if (requiredUrls?.length) {
-                    const missingUrls = requiredUrls.filter((url) => !permissionUtils.canLoadResource(type, url));
-                    if (missingUrls.length > 0) {
-                        missingExternal[type] = missingUrls;
-                        hasAllRequiredPermissions = false;
-                    }
-                }
-            });
-            if (Object.keys(missingExternal).length > 0) {
-                missing.external = missingExternal;
+            try {
+                setError(null); // Clear any previous errors
+                const result = await (0, bridge_1.checkPermissions)(requiredPermissions, context.permissions);
+                setPermissionResult(result);
             }
-        }
-        // Note: Content permissions are not supported in the current RuntimePermissions type
-        return {
-            granted: hasAllRequiredPermissions,
-            missing: hasAllRequiredPermissions ? null : missing
+            catch (err) {
+                setError(err instanceof Error ? err : new Error('Failed to check permissions'));
+                setPermissionResult({ granted: false, missing: null });
+            }
         };
-    }, [permissionUtils, requiredPermissions]);
+        void checkPerms();
+    }, [context, requiredPermissions, isLoading]);
     return {
         hasPermission: permissionResult.granted,
         isLoading,
         missingPermissions: permissionResult.missing,
Index: package/out/hooks/__test__/usePermissions.test.js
===================================================================
--- package/out/hooks/__test__/usePermissions.test.js
+++ package/out/hooks/__test__/usePermissions.test.js
@@ -3,13 +3,27 @@
 const react_hooks_1 = require("@testing-library/react-hooks");
 const usePermissions_1 = require("../usePermissions");
 const testUtils_1 = require("../../__test__/testUtils");
 // Mock @forge/bridge
-jest.mock('@forge/bridge', () => ({
-    view: {
-        getContext: jest.fn()
+jest.mock('@forge/bridge', () => {
+    // Set up window before requiring actual bridge to avoid initialization issues
+    if (typeof window === 'undefined') {
+        // @ts-ignore
+        global.window = global;
+        // @ts-ignore
+        global.window.__bridge = {
+            callBridge: jest.fn()
+        };
     }
-}));
+    const actualBridge = jest.requireActual('@forge/bridge');
+    return {
+        ...actualBridge,
+        view: {
+            ...actualBridge.view,
+            getContext: jest.fn()
+        }
+    };
+});
 const mockGetContext = jest.fn();
 describe('usePermissions', () => {
     beforeEach(() => {
         jest.clearAllMocks();
Index: package/package.json
===================================================================
--- package/package.json
+++ package/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@forge/react",
-  "version": "11.9.2-next.0-experimental-919607a",
+  "version": "11.10.0-next.1",
   "description": "Forge React reconciler",
   "author": "Atlassian",
   "license": "SEE LICENSE IN LICENSE.txt",
   "main": "out/index.js",
@@ -27,9 +27,9 @@
   "dependencies": {
     "@atlaskit/adf-schema": "^48.0.0",
     "@atlaskit/adf-utils": "^19.19.0",
     "@atlaskit/forge-react-types": "^0.48.0",
-    "@forge/bridge": "^5.10.3-next.1-experimental-919607a",
+    "@forge/bridge": "^5.11.0-next.2",
     "@forge/egress": "^2.3.1",
     "@forge/i18n": "0.0.7",
     "@types/react": "^18.2.64",
     "@types/react-reconciler": "^0.28.8",
Index: package/out/hooks/usePermissions.d.ts.map
===================================================================
--- package/out/hooks/usePermissions.d.ts.map
+++ package/out/hooks/usePermissions.d.ts.map
@@ -1,1 +1,1 @@
-{"version":3,"file":"usePermissions.d.ts","sourceRoot":"","sources":["../../src/hooks/usePermissions.ts"],"names":[],"mappings":"AAuBA;;GAEG;AACH,QAAA,MAAM,cAAc,sEAAuE,CAAC;AAC5F,oBAAY,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D;;GAEG;AACH,QAAA,MAAM,WAAW,gCAAiC,CAAC;AACnD,oBAAY,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE;QACT,KAAK,CAAC,EAAE;YACN,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;YACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;SACnB,CAAC;QACF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IACF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,oBAAY,sBAAsB,GAAG,WAAW,CAAC;AAEjD;;GAEG;AACH,oBAAY,kBAAkB,GAAG,WAAW,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACpC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,cAAc,wBAAyB,sBAAsB;;;;;CAqJzE,CAAC"}
\ No newline at end of file
+{"version":3,"file":"usePermissions.d.ts","sourceRoot":"","sources":["../../src/hooks/usePermissions.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,KAAK,SAAS,EACf,MAAM,eAAe,CAAC;AAGvB,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC;AAExC,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE;QACT,KAAK,CAAC,EAAE;YACN,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;YACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;SACnB,CAAC;QACF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IACF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAGD,YAAY,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,cAAc,wBAAyB,sBAAsB;;;;;CAoEzE,CAAC"}
\ No newline at end of file
Index: package/CHANGELOG.md
===================================================================
--- package/CHANGELOG.md
+++ package/CHANGELOG.md
@@ -1,11 +1,16 @@
 # @forge/react
 
-## 11.9.2-next.0-experimental-919607a
+## 11.10.0-next.1
 
+### Minor Changes
+
+- f058dd8: Expose Permissions API in @forge/bridge for Custom UI apps
+
 ### Patch Changes
 
-- @forge/[email protected]
+- Updated dependencies [f058dd8]
+  - @forge/[email protected]
 
 ## 11.9.2-next.0
 
 ### Patch Changes
Index: package/out/hooks/usePermissions.d.ts
===================================================================
--- package/out/hooks/usePermissions.d.ts
+++ package/out/hooks/usePermissions.d.ts
@@ -1,14 +1,6 @@
-/**
- * Resource types that can be loaded externally
- */
-declare const RESOURCE_TYPES: readonly ["fonts", "styles", "frames", "images", "media", "scripts"];
-export declare type ResourceType = (typeof RESOURCE_TYPES)[number];
-/**
- * Fetch types for external requests
- */
-declare const FETCH_TYPES: readonly ["backend", "client"];
-export declare type FetchType = (typeof FETCH_TYPES)[number];
+import { type PermissionRequirements, type MissingPermissions, type PermissionCheckResult, type ResourceType, type FetchType } from '@forge/bridge';
+export type { ResourceType, FetchType };
 export interface Permissions {
     scopes?: string[];
     external?: {
         fetch?: {
@@ -23,24 +15,10 @@
         scripts?: string[];
     };
     content?: Record<string, unknown>;
 }
+export type { PermissionRequirements, MissingPermissions, PermissionCheckResult };
 /**
- * Required permissions for a component
- */
-export declare type PermissionRequirements = Permissions;
-/**
- * Missing permissions information
- */
-export declare type MissingPermissions = Permissions;
-/**
- * Permission check result
- */
-export interface PermissionCheckResult {
-    granted: boolean;
-    missing: MissingPermissions | null;
-}
-/**
  * Hook for checking permissions in Forge apps
  *
  * @param requiredPermissions - The permissions required for the component
  * @returns Object containing permission state, loading status, and error information
@@ -77,9 +55,8 @@
  */
 export declare const usePermissions: (requiredPermissions: PermissionRequirements) => {
     hasPermission: boolean;
     isLoading: boolean;
-    missingPermissions: Permissions | null;
+    missingPermissions: PermissionRequirements | null;
     error: Error | null;
 };
-export {};
 //# sourceMappingURL=usePermissions.d.ts.map
\ No newline at end of file
File too large for inline diff