@microsoft/power-apps-native-host 0.2.25 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -10
- package/compatibility/current.json +31 -4
- package/lib/NOTICE.txt +1325 -24
- package/lib/commonjs/auth/AuthContext.js +110 -33
- package/lib/commonjs/bridge/NativePowerAppsBridge.js +11 -18
- package/lib/commonjs/bridge/PowerPagesBridge.js +27 -8
- package/lib/commonjs/bridge/bridgeState.js +40 -0
- package/lib/commonjs/config/babelConfig.js +13 -0
- package/lib/commonjs/config/expoConfig.js +71 -0
- package/lib/commonjs/config/metroConfig.js +70 -0
- package/lib/commonjs/config/metroLogger.js +182 -0
- package/lib/commonjs/config/tamaguiConfig.js +98 -0
- package/lib/commonjs/connections/ConnectionSetupScreen.js +459 -80
- package/lib/commonjs/connections/PowerPlatformConnectionApi.js +946 -0
- package/lib/commonjs/connections/connectionMetadata.js +127 -0
- package/lib/commonjs/connections/connectionState.js +96 -0
- package/lib/commonjs/connections/disambiguate.js +19 -11
- package/lib/commonjs/connections/resolveConnections.js +348 -0
- package/lib/commonjs/connections/types.js +106 -0
- package/lib/commonjs/{platform → connections}/useConnectionRefs.js +130 -65
- package/lib/commonjs/connections/useConnectionSetup.js +111 -52
- package/lib/commonjs/controls/FilePicker.js +6 -0
- package/lib/commonjs/controls/ImagePicker.js +8 -0
- package/lib/commonjs/index.js +167 -4
- package/lib/commonjs/logger.js +9 -6
- package/lib/commonjs/offline/createOfflineHostActivationCallbacks.js +61 -0
- package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.js +48 -0
- package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.web.js +13 -0
- package/lib/commonjs/platform/DataverseDataSources.js +18 -0
- package/lib/commonjs/platform/DataverseGlobalDiscoveryApi.js +94 -0
- package/lib/commonjs/platform/GlobalDiscovery.js +51 -0
- package/lib/commonjs/platform/PowerPlatformEnvironmentApi.js +6 -106
- package/lib/commonjs/provider/PowerAppsHostProvider.js +157 -25
- package/lib/commonjs/provider/PowerAppsProvider.js +178 -134
- package/lib/commonjs/provider/SessionDetailsGesture.js +208 -0
- package/lib/commonjs/telemetry/OfflineSdkTelemetry.js +95 -0
- package/lib/commonjs/telemetry/OfflineSdkTelemetry.web.js +9 -0
- package/lib/commonjs/telemetry/index.js +22 -3
- package/lib/module/auth/AuthContext.js +110 -33
- package/lib/module/bridge/NativePowerAppsBridge.js +12 -19
- package/lib/module/bridge/PowerPagesBridge.js +27 -8
- package/lib/module/bridge/bridgeState.js +34 -0
- package/lib/module/config/babelConfig.js +9 -0
- package/lib/module/config/expoConfig.js +67 -0
- package/lib/module/config/metroConfig.js +65 -0
- package/lib/module/config/metroLogger.js +175 -0
- package/lib/module/config/tamaguiConfig.js +93 -0
- package/lib/module/connections/ConnectionSetupScreen.js +460 -81
- package/lib/module/connections/PowerPlatformConnectionApi.js +932 -0
- package/lib/module/connections/connectionMetadata.js +112 -0
- package/lib/module/connections/connectionState.js +82 -0
- package/lib/module/connections/disambiguate.js +19 -12
- package/lib/module/connections/resolveConnections.js +343 -0
- package/lib/module/connections/types.js +91 -0
- package/lib/module/{platform → connections}/useConnectionRefs.js +131 -66
- package/lib/module/connections/useConnectionSetup.js +113 -54
- package/lib/module/controls/FilePicker.js +6 -0
- package/lib/module/controls/ImagePicker.js +8 -0
- package/lib/module/index.js +14 -4
- package/lib/module/logger.js +8 -6
- package/lib/module/offline/createOfflineHostActivationCallbacks.js +55 -0
- package/lib/module/offline/useOfflineIntegrationWithStartupGate.js +44 -0
- package/lib/module/offline/useOfflineIntegrationWithStartupGate.web.js +9 -0
- package/lib/module/platform/DataverseDataSources.js +13 -0
- package/lib/module/platform/DataverseGlobalDiscoveryApi.js +89 -0
- package/lib/module/platform/GlobalDiscovery.js +45 -0
- package/lib/module/platform/PowerPlatformEnvironmentApi.js +5 -104
- package/lib/module/provider/PowerAppsHostProvider.js +160 -28
- package/lib/module/provider/PowerAppsProvider.js +182 -138
- package/lib/module/provider/SessionDetailsGesture.js +203 -0
- package/lib/module/telemetry/OfflineSdkTelemetry.js +91 -0
- package/lib/module/telemetry/OfflineSdkTelemetry.web.js +5 -0
- package/lib/module/telemetry/index.js +15 -4
- package/lib/typescript/commonjs/auth/AuthContext.d.ts +3 -1
- package/lib/typescript/commonjs/auth/AuthContext.web.d.ts +1 -0
- package/lib/typescript/commonjs/bridge/bridgeState.d.ts +5 -0
- package/lib/typescript/commonjs/config/babelConfig.d.ts +12 -0
- package/lib/typescript/commonjs/config/expoConfig.d.ts +21 -0
- package/lib/typescript/commonjs/config/metroConfig.d.ts +6 -0
- package/lib/typescript/commonjs/config/metroLogger.d.ts +21 -0
- package/lib/typescript/commonjs/config/tamaguiConfig.d.ts +1792 -0
- package/lib/typescript/commonjs/connections/ConnectionSetupScreen.d.ts +28 -7
- package/lib/typescript/commonjs/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
- package/lib/typescript/commonjs/connections/connectionMetadata.d.ts +66 -0
- package/lib/typescript/commonjs/connections/connectionState.d.ts +16 -0
- package/lib/typescript/commonjs/connections/disambiguate.d.ts +4 -2
- package/lib/typescript/commonjs/{platform → connections}/resolveConnections.d.ts +22 -20
- package/lib/typescript/commonjs/connections/types.d.ts +129 -1
- package/lib/typescript/commonjs/{platform → connections}/useConnectionRefs.d.ts +36 -25
- package/lib/typescript/commonjs/connections/useConnectionSetup.d.ts +12 -8
- package/lib/typescript/commonjs/controls/FilePicker.d.ts +7 -1
- package/lib/typescript/commonjs/controls/ImagePicker.d.ts +9 -1
- package/lib/typescript/commonjs/index.d.ts +18 -10
- package/lib/typescript/commonjs/logger.d.ts +4 -4
- package/lib/typescript/commonjs/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
- package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
- package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
- package/lib/typescript/commonjs/platform/DataverseDataSources.d.ts +28 -0
- package/lib/typescript/commonjs/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
- package/lib/typescript/commonjs/platform/GlobalDiscovery.d.ts +4 -0
- package/lib/typescript/commonjs/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
- package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +29 -18
- package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +54 -9
- package/lib/typescript/commonjs/provider/SessionDetailsGesture.d.ts +13 -0
- package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.d.ts +15 -0
- package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
- package/lib/typescript/commonjs/telemetry/index.d.ts +13 -1
- package/lib/typescript/module/auth/AuthContext.d.ts +3 -1
- package/lib/typescript/module/auth/AuthContext.web.d.ts +1 -0
- package/lib/typescript/module/bridge/bridgeState.d.ts +5 -0
- package/lib/typescript/module/config/babelConfig.d.ts +12 -0
- package/lib/typescript/module/config/expoConfig.d.ts +21 -0
- package/lib/typescript/module/config/metroConfig.d.ts +6 -0
- package/lib/typescript/module/config/metroLogger.d.ts +21 -0
- package/lib/typescript/module/config/tamaguiConfig.d.ts +1792 -0
- package/lib/typescript/module/connections/ConnectionSetupScreen.d.ts +28 -7
- package/lib/typescript/module/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
- package/lib/typescript/module/connections/connectionMetadata.d.ts +66 -0
- package/lib/typescript/module/connections/connectionState.d.ts +16 -0
- package/lib/typescript/module/connections/disambiguate.d.ts +4 -2
- package/lib/typescript/module/{platform → connections}/resolveConnections.d.ts +22 -20
- package/lib/typescript/module/connections/types.d.ts +129 -1
- package/lib/typescript/module/{platform → connections}/useConnectionRefs.d.ts +36 -25
- package/lib/typescript/module/connections/useConnectionSetup.d.ts +12 -8
- package/lib/typescript/module/controls/FilePicker.d.ts +7 -1
- package/lib/typescript/module/controls/ImagePicker.d.ts +9 -1
- package/lib/typescript/module/index.d.ts +18 -10
- package/lib/typescript/module/logger.d.ts +4 -4
- package/lib/typescript/module/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
- package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
- package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
- package/lib/typescript/module/platform/DataverseDataSources.d.ts +28 -0
- package/lib/typescript/module/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
- package/lib/typescript/module/platform/GlobalDiscovery.d.ts +4 -0
- package/lib/typescript/module/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
- package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +29 -18
- package/lib/typescript/module/provider/PowerAppsProvider.d.ts +54 -9
- package/lib/typescript/module/provider/SessionDetailsGesture.d.ts +13 -0
- package/lib/typescript/module/telemetry/OfflineSdkTelemetry.d.ts +15 -0
- package/lib/typescript/module/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
- package/lib/typescript/module/telemetry/index.d.ts +13 -1
- package/package.json +52 -13
- package/scripts/build-codegen-package.js +283 -0
- package/scripts/start-web.mjs +9 -5
- package/scripts/upgrade/runtime.js +139 -24
- package/scripts/upgrade-authoring.js +274 -180
- package/scripts/upgrade.js +10 -16
- package/src/config/generated-config.d.ts +4 -0
- package/src/config/index.d.ts +3 -0
- package/src/config/tsconfig.json +26 -0
- package/lib/commonjs/platform/PowerPlatformConnectionApi.js +0 -606
- package/lib/commonjs/platform/resolveConnections.js +0 -264
- package/lib/module/platform/PowerPlatformConnectionApi.js +0 -595
- package/lib/module/platform/resolveConnections.js +0 -259
|
@@ -21,6 +21,97 @@ export let ConnectionStatus = /*#__PURE__*/function (ConnectionStatus) {
|
|
|
21
21
|
ConnectionStatus["NotConnected"] = "NotConnected";
|
|
22
22
|
return ConnectionStatus;
|
|
23
23
|
}({});
|
|
24
|
+
export function normalizeApiId(apiId) {
|
|
25
|
+
const connectorId = apiId.split('/').filter(Boolean).pop() ?? apiId;
|
|
26
|
+
return `/providers/Microsoft.PowerApps/apis/${connectorId.toLowerCase()}`;
|
|
27
|
+
}
|
|
28
|
+
export function getConnectorId(apiId) {
|
|
29
|
+
return (apiId.split('/').filter(Boolean).pop() ?? apiId).toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
export function getConnectionDatasetName(reference) {
|
|
32
|
+
const datasetNames = Object.keys(reference.dataSets ?? {});
|
|
33
|
+
return datasetNames.length === 1 ? datasetNames[0] : '';
|
|
34
|
+
}
|
|
35
|
+
export function getFlowConnectionName(reference) {
|
|
36
|
+
const workflowNameHint = reference.parameterHintsV2?.workflowName?.value;
|
|
37
|
+
const workflowEntityIdHint = reference.parameterHintsV2?.workflowEntityId?.value;
|
|
38
|
+
return reference.workflowDetails?.workflowName ?? (typeof workflowNameHint === 'string' ? workflowNameHint : undefined) ?? reference.workflowDetails?.workflowEntityId ?? (typeof workflowEntityIdHint === 'string' ? workflowEntityIdHint : '');
|
|
39
|
+
}
|
|
40
|
+
export function getFlowDataSourceNames(reference) {
|
|
41
|
+
const names = new Set(reference.dataSources);
|
|
42
|
+
const workflowDisplayNameHint = reference.parameterHintsV2?.workflowDisplayName?.value;
|
|
43
|
+
const displayName = reference.workflowDetails?.workflowDisplayName ?? (typeof workflowDisplayNameHint === 'string' ? workflowDisplayNameHint : undefined);
|
|
44
|
+
if (displayName) names.add(`${displayName.charAt(0).toLowerCase()}${displayName.slice(1)}`);
|
|
45
|
+
return [...names];
|
|
46
|
+
}
|
|
47
|
+
export function buildConnectionRequirements(connectionReferences, scopesByApiId = {}) {
|
|
48
|
+
return Object.entries(connectionReferences).filter(([, reference]) => !isCompositeConnectionApiId(reference.id)).map(([localRefId, reference]) => {
|
|
49
|
+
const appActions = new Set(reference.appActions ?? []);
|
|
50
|
+
let isFlowDependent = false;
|
|
51
|
+
for (const dependentId of reference.dependents ?? []) {
|
|
52
|
+
const dependent = connectionReferences[dependentId];
|
|
53
|
+
if (!dependent || getConnectorId(dependent.id) !== 'shared_logicflows') continue;
|
|
54
|
+
isFlowDependent = true;
|
|
55
|
+
for (const action of dependent.nestedActions ?? []) {
|
|
56
|
+
const hint = action.flowConnectionRef ? dependent.parameterHintsV2?.[action.flowConnectionRef] : undefined;
|
|
57
|
+
if (hint?.value === localRefId && action.actionName) appActions.add(action.actionName);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const apiId = normalizeApiId(reference.id);
|
|
61
|
+
return {
|
|
62
|
+
localRefId,
|
|
63
|
+
apiId,
|
|
64
|
+
connectorId: getConnectorId(reference.id),
|
|
65
|
+
dataSources: reference.dataSources,
|
|
66
|
+
displayName: reference.displayName,
|
|
67
|
+
iconUri: reference.iconUri,
|
|
68
|
+
appActions: [...appActions],
|
|
69
|
+
sharedConnectionId: reference.sharedConnectionId,
|
|
70
|
+
gatewayObjectIdHint: reference.gatewayObjectIdHint,
|
|
71
|
+
authenticationType: reference.authenticationType,
|
|
72
|
+
parameterSetName: reference.parameterSetName,
|
|
73
|
+
apiScopes: scopesByApiId[apiId] ?? {
|
|
74
|
+
will: [],
|
|
75
|
+
wont: []
|
|
76
|
+
},
|
|
77
|
+
isFlowDependent
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export function isCompositeConnectionApiId(apiId) {
|
|
82
|
+
const connectorId = getConnectorId(apiId);
|
|
83
|
+
return connectorId === 'shared_excel' || connectorId === 'shared_logicflows';
|
|
84
|
+
}
|
|
85
|
+
export function findConnectionDataSourceConflicts(requirements) {
|
|
86
|
+
const owners = new Map();
|
|
87
|
+
const conflicts = new Set();
|
|
88
|
+
for (const requirement of requirements) {
|
|
89
|
+
for (const dataSource of requirement.dataSources) {
|
|
90
|
+
const owner = owners.get(dataSource);
|
|
91
|
+
if (owner && owner !== requirement.localRefId) conflicts.add(dataSource);else owners.set(dataSource, requirement.localRefId);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return [...conflicts];
|
|
95
|
+
}
|
|
96
|
+
export function hasCompleteStaticConnectionConfig(connectionReferences, schemaMap) {
|
|
97
|
+
const references = Object.values(connectionReferences);
|
|
98
|
+
const directReferences = references.filter(reference => !isCompositeConnectionApiId(reference.id));
|
|
99
|
+
return directReferences.length > 0 && directReferences.every(reference => Boolean(reference.id) && reference.dataSources.length > 0 && reference.dataSources.every(dataSource => Boolean(schemaMap[dataSource]?.name && schemaMap[dataSource]?.properties?.primaryRuntimeUrl)));
|
|
100
|
+
}
|
|
101
|
+
export function buildConnectionResolutionKey(appId, environmentId, connectionReferences) {
|
|
102
|
+
const references = Object.entries(connectionReferences).sort(([left], [right]) => left.localeCompare(right)).map(([localRefId, reference]) => ({
|
|
103
|
+
localRefId,
|
|
104
|
+
id: reference.id,
|
|
105
|
+
dataSources: [...reference.dataSources].sort(),
|
|
106
|
+
sharedConnectionId: reference.sharedConnectionId ?? '',
|
|
107
|
+
parameterSetName: reference.parameterSetName ?? ''
|
|
108
|
+
}));
|
|
109
|
+
return JSON.stringify({
|
|
110
|
+
appId: appId ?? '',
|
|
111
|
+
environmentId,
|
|
112
|
+
references
|
|
113
|
+
});
|
|
114
|
+
}
|
|
24
115
|
|
|
25
116
|
/** A single connection instance returned by the Power Platform connectivity API. */
|
|
26
117
|
|
|
@@ -35,13 +35,14 @@
|
|
|
35
35
|
|
|
36
36
|
import { useState, useEffect, useRef } from 'react';
|
|
37
37
|
import { isDevPlayer } from "../utils/isDevPlayer.js";
|
|
38
|
-
import { powerPlatformScope } from "
|
|
39
|
-
import {
|
|
40
|
-
import { fetchAppLaunchData } from "./PowerPlatformConnectionApi.js";
|
|
38
|
+
import { powerPlatformScope } from "../platform/PowerPlatformEnvironmentApi.js";
|
|
39
|
+
import { AppLaunchAccessDeniedError, fetchAppLaunchData } from "./PowerPlatformConnectionApi.js";
|
|
41
40
|
import { applyAclGrants, resolveViaStaticRefs } from "./resolveConnections.js";
|
|
41
|
+
import { applyRegisteredConnectionUsages, getConnectionNamesRequiringAclGrant, invalidateFailedAclGrants, selectLaunchConnection } from "./connectionState.js";
|
|
42
42
|
import { debugLog, debugWarn } from "../logger.js";
|
|
43
43
|
import { getAppLogger, HostOperationNames } from "../telemetry/index.js";
|
|
44
44
|
import { buildConnectionReferences } from "../bridge/NativePowerAppsBridge.js";
|
|
45
|
+
import { buildConnectionResolutionKey } from "./types.js";
|
|
45
46
|
|
|
46
47
|
// Re-exported for backward compatibility with consumers that import ConnectorSchema
|
|
47
48
|
// from this module (e.g. via the package index).
|
|
@@ -51,17 +52,10 @@ import { buildConnectionReferences } from "../bridge/NativePowerAppsBridge.js";
|
|
|
51
52
|
* disambiguation support for environments where multiple connections exist for
|
|
52
53
|
* the same connector.
|
|
53
54
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* 2. Acquires a Power Platform API token.
|
|
59
|
-
* 3. In parallel: fetches the Dataverse instanceUrl and ALL connections for
|
|
60
|
-
* each configured connector.
|
|
61
|
-
* 4. Runs disambiguation: resolves single matches, invokes `onDisambiguationNeeded`
|
|
62
|
-
* for multiple matches, and invokes `onMissingConnection` for absent ones.
|
|
63
|
-
* 5. Pre-warms the Dataverse token in the MSAL cache.
|
|
64
|
-
* 6. Sets isReady=true with the fully-populated connections map.
|
|
55
|
+
* Outside Dev Player, the published app launch endpoint is authoritative.
|
|
56
|
+
* Dev Player resolves unpublished local references through per-connector APIs.
|
|
57
|
+
* Both paths disambiguate candidates, pre-warm Dataverse authentication, and
|
|
58
|
+
* return a fully populated bridge configuration.
|
|
65
59
|
*/
|
|
66
60
|
export function useConnectionRefs({
|
|
67
61
|
isSignedIn,
|
|
@@ -70,6 +64,7 @@ export function useConnectionRefs({
|
|
|
70
64
|
environmentId,
|
|
71
65
|
region = 'prod',
|
|
72
66
|
connectionReferences,
|
|
67
|
+
hasDataverse,
|
|
73
68
|
schemaMap,
|
|
74
69
|
onDisambiguationNeeded,
|
|
75
70
|
onMissingConnection,
|
|
@@ -78,31 +73,37 @@ export function useConnectionRefs({
|
|
|
78
73
|
const [connectionRefs, setConnectionRefs] = useState({});
|
|
79
74
|
const [isReady, setIsReady] = useState(false);
|
|
80
75
|
const [error, setError] = useState(null);
|
|
76
|
+
const [errorKind, setErrorKind] = useState(null);
|
|
81
77
|
const [missingConnectors, setMissingConnectors] = useState([]);
|
|
78
|
+
const [missingConnectionReferences, setMissingConnectionReferences] = useState([]);
|
|
79
|
+
const [connectionRequirements, setConnectionRequirements] = useState([]);
|
|
80
|
+
const [resourceConsents, setResourceConsents] = useState([]);
|
|
81
|
+
const [configurationErrors, setConfigurationErrors] = useState([]);
|
|
82
|
+
const [requiresConnectionConsent, setRequiresConnectionConsent] = useState(false);
|
|
83
|
+
const [resolvedConfigKey, setResolvedConfigKey] = useState(null);
|
|
82
84
|
const [dataverseOrgUrl, setDataverseOrgUrl] = useState('');
|
|
85
|
+
const [databaseReferences, setDatabaseReferences] = useState({});
|
|
83
86
|
|
|
84
|
-
// Bounded re-resolution for
|
|
85
|
-
// cold boot is usually a silent-auth miss (the account exists, but MSAL hasn't
|
|
86
|
-
// produced a token yet); without a retry the effect — keyed on isSignedIn —
|
|
87
|
-
// would never re-resolve once a token becomes available, leaving the bridge on
|
|
88
|
-
// the static fallback refs (no runtimeUrl) for the whole session. retryTick
|
|
89
|
-
// re-runs the effect; tokenRetryCountRef caps the attempts so it can never loop.
|
|
87
|
+
// Bounded re-resolution for transient silent-token misses.
|
|
90
88
|
const [retryTick, setRetryTick] = useState(0);
|
|
91
89
|
const tokenRetryCountRef = useRef(0);
|
|
92
|
-
//
|
|
93
|
-
// retry below (not an initial resolution or a sign-in). Used to avoid
|
|
94
|
-
// clobbering an already-ready UI: a retry must re-resolve in the background
|
|
95
|
-
// without dropping isReady, which would otherwise unmount the whole signed-in
|
|
96
|
-
// subtree (spinner) on every tick.
|
|
90
|
+
// Dev Player retries in the background without dropping its locally ready UI.
|
|
97
91
|
const isBackgroundRetryRef = useRef(false);
|
|
98
92
|
const MAX_TOKEN_RETRIES = 5;
|
|
99
93
|
const TOKEN_RETRY_DELAY_MS = 2000;
|
|
94
|
+
const currentConfigKey = buildConnectionResolutionKey(appId, environmentId, isDevPlayer ? connectionReferences : {});
|
|
100
95
|
useEffect(() => {
|
|
101
96
|
if (!isSignedIn) {
|
|
102
97
|
tokenRetryCountRef.current = 0;
|
|
103
98
|
isBackgroundRetryRef.current = false;
|
|
104
99
|
return;
|
|
105
100
|
}
|
|
101
|
+
if (!isDevPlayer && (!appId || !environmentId)) {
|
|
102
|
+
setErrorKind('generic');
|
|
103
|
+
setError('The production app requires both an app ID and an environment ID before it can load from Power Platform.');
|
|
104
|
+
setIsReady(true);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
106
107
|
|
|
107
108
|
// Consume the background-retry flag once per run so a later initial run or
|
|
108
109
|
// sign-in can never inherit a stale value.
|
|
@@ -120,6 +121,7 @@ export function useConnectionRefs({
|
|
|
120
121
|
// an online (offline-disabled) played app never resolves its instanceUrl in
|
|
121
122
|
// the Dev Player and every Dataverse read fails with "No instanceUrl found".
|
|
122
123
|
if (isDevPlayer && !hasConnectionRefs && !environmentId) {
|
|
124
|
+
setResolvedConfigKey(currentConfigKey);
|
|
123
125
|
setIsReady(true);
|
|
124
126
|
return;
|
|
125
127
|
}
|
|
@@ -128,50 +130,63 @@ export function useConnectionRefs({
|
|
|
128
130
|
// (and no appId to fetch from the launch API).
|
|
129
131
|
// Mark ready immediately so the bridge can initialise without any network calls.
|
|
130
132
|
if (!environmentId && !appId) {
|
|
133
|
+
setResolvedConfigKey(currentConfigKey);
|
|
131
134
|
setIsReady(true);
|
|
132
135
|
return;
|
|
133
136
|
}
|
|
134
137
|
let cancelled = false;
|
|
135
138
|
let retryTimer;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
const resolutionScenario = getAppLogger().trackScenario(HostOperationNames.PlatformResolveConnectionRefs, {
|
|
140
|
+
mode: isDevPlayer ? 'devPlayer' : 'publishedApp',
|
|
141
|
+
hasAppId: Boolean(appId),
|
|
142
|
+
connectionReferenceCount: Object.keys(connectionReferences).length
|
|
143
|
+
});
|
|
144
|
+
// Preserve the locally ready Dev Player UI during a background retry.
|
|
140
145
|
if (!isBackgroundRetry) {
|
|
141
146
|
setIsReady(false);
|
|
142
147
|
setError(null);
|
|
148
|
+
setErrorKind(null);
|
|
143
149
|
setMissingConnectors([]);
|
|
150
|
+
setMissingConnectionReferences([]);
|
|
151
|
+
setConnectionRequirements([]);
|
|
152
|
+
setResourceConsents([]);
|
|
153
|
+
setConfigurationErrors([]);
|
|
154
|
+
setRequiresConnectionConsent(false);
|
|
155
|
+
setResolvedConfigKey(null);
|
|
156
|
+
setDataverseOrgUrl('');
|
|
157
|
+
setDatabaseReferences({});
|
|
144
158
|
}
|
|
145
159
|
async function loadConnections() {
|
|
146
160
|
const token = await acquireToken([powerPlatformScope(region)]);
|
|
147
|
-
if (cancelled)
|
|
161
|
+
if (cancelled) {
|
|
162
|
+
resolutionScenario.complete({
|
|
163
|
+
result: 'cancelled'
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
148
167
|
if (!token) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// Still mark ready so the host mounts the UI (the offline SDK serves local
|
|
160
|
-
// SQLite without a token) instead of gating on !isReady forever.
|
|
161
|
-
setConnectionRefs(buildConnectionReferences(connectionReferences));
|
|
162
|
-
setIsReady(true);
|
|
163
|
-
// Schedule a bounded re-resolution so a transient silent-auth miss
|
|
164
|
-
// recovers once a token is available, instead of staying on the static
|
|
165
|
-
// fallback refs for the rest of the session. Capped by MAX_TOKEN_RETRIES
|
|
166
|
-
// so it can never spin.
|
|
168
|
+
resolutionScenario.completeWithError({
|
|
169
|
+
reason: 'tokenUnavailable'
|
|
170
|
+
});
|
|
171
|
+
// Dev Player can render local metadata while token recovery continues;
|
|
172
|
+
// standalone apps stay blocked because production has no static fallback.
|
|
173
|
+
if (isDevPlayer) {
|
|
174
|
+
setConnectionRefs(buildConnectionReferences(connectionReferences));
|
|
175
|
+
setIsReady(true);
|
|
176
|
+
setResolvedConfigKey(currentConfigKey);
|
|
177
|
+
}
|
|
167
178
|
if (tokenRetryCountRef.current < MAX_TOKEN_RETRIES) {
|
|
168
179
|
tokenRetryCountRef.current += 1;
|
|
169
180
|
retryTimer = setTimeout(() => {
|
|
170
181
|
if (!cancelled) {
|
|
171
|
-
isBackgroundRetryRef.current =
|
|
182
|
+
isBackgroundRetryRef.current = isDevPlayer;
|
|
172
183
|
setRetryTick(t => t + 1);
|
|
173
184
|
}
|
|
174
185
|
}, TOKEN_RETRY_DELAY_MS);
|
|
186
|
+
} else if (!isDevPlayer) {
|
|
187
|
+
setErrorKind('generic');
|
|
188
|
+
setError('Unable to authenticate with Power Platform to load this app. Sign in again and retry.');
|
|
189
|
+
setIsReady(true);
|
|
175
190
|
}
|
|
176
191
|
return;
|
|
177
192
|
}
|
|
@@ -185,30 +200,50 @@ export function useConnectionRefs({
|
|
|
185
200
|
let result = null;
|
|
186
201
|
if (appId && environmentId && !isDevPlayer) {
|
|
187
202
|
result = await fetchAppLaunchData(appId, environmentId, region, token);
|
|
203
|
+
if (!result) {
|
|
204
|
+
throw new Error(`App "${appId}" is not available in environment "${environmentId}". Push the mobile app to this environment and try again.`);
|
|
205
|
+
}
|
|
206
|
+
if (result && onDisambiguationNeeded) {
|
|
207
|
+
for (const requirement of [...result.missingConnectionReferences]) {
|
|
208
|
+
const candidates = result.connectionCandidates[requirement.localRefId] ?? [];
|
|
209
|
+
if (candidates.length < 2) continue;
|
|
210
|
+
const selectedId = await onDisambiguationNeeded({
|
|
211
|
+
localRefId: requirement.localRefId,
|
|
212
|
+
apiId: requirement.apiId,
|
|
213
|
+
candidates
|
|
214
|
+
});
|
|
215
|
+
if (selectedId) {
|
|
216
|
+
selectLaunchConnection(result, requirement.localRefId, selectedId);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
188
220
|
if (result && isvClientId) {
|
|
189
|
-
await applyAclGrants(result.connectionRefs, environmentId,
|
|
190
|
-
|
|
191
|
-
|
|
221
|
+
const failedConnectionNames = await applyAclGrants(result.connectionRefs, result.connectionRequirements, environmentId, region, acquireToken);
|
|
222
|
+
invalidateFailedAclGrants(result, failedConnectionNames);
|
|
223
|
+
} else if (result) {
|
|
224
|
+
debugWarn('[useConnectionRefs] could not grant user-selected connections; treating them as setup-required');
|
|
192
225
|
getAppLogger().trackErrorEvent(HostOperationNames.PlatformConnectionAclGrant, {
|
|
193
226
|
phase: 'launchAclVerify',
|
|
194
|
-
reason:
|
|
195
|
-
connectionRefCount:
|
|
227
|
+
reason: 'missingIsvClientId',
|
|
228
|
+
connectionRefCount: Object.keys(result.connectionRefs).length
|
|
196
229
|
});
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
}
|
|
230
|
+
const unresolved = new Set(getConnectionNamesRequiringAclGrant(result.connectionRefs, result.connectionRequirements));
|
|
231
|
+
invalidateFailedAclGrants(result, unresolved);
|
|
232
|
+
}
|
|
233
|
+
if (result) {
|
|
234
|
+
applyRegisteredConnectionUsages(result, result.registeredConnectionUsages);
|
|
204
235
|
}
|
|
205
236
|
}
|
|
206
237
|
|
|
207
238
|
// Slow path: per-connector resolution.
|
|
208
|
-
// Used
|
|
239
|
+
// Used only in Dev Player, where unpublished local app metadata is expected.
|
|
209
240
|
if (!result) {
|
|
241
|
+
if (!isDevPlayer) {
|
|
242
|
+
throw new Error('Unable to load the production app from the Power Platform launch endpoint.');
|
|
243
|
+
}
|
|
210
244
|
result = await resolveViaStaticRefs({
|
|
211
245
|
connectionReferences,
|
|
246
|
+
hasDataverse,
|
|
212
247
|
schemaMap,
|
|
213
248
|
environmentId,
|
|
214
249
|
region,
|
|
@@ -219,7 +254,12 @@ export function useConnectionRefs({
|
|
|
219
254
|
onMissingConnection
|
|
220
255
|
});
|
|
221
256
|
}
|
|
222
|
-
if (cancelled)
|
|
257
|
+
if (cancelled) {
|
|
258
|
+
resolutionScenario.complete({
|
|
259
|
+
result: 'cancelled'
|
|
260
|
+
});
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
223
263
|
|
|
224
264
|
// Pre-warm the Dataverse token while the user waits.
|
|
225
265
|
if (result.dataverseOrgUrl) {
|
|
@@ -228,8 +268,20 @@ export function useConnectionRefs({
|
|
|
228
268
|
if (!cancelled) {
|
|
229
269
|
setConnectionRefs(result.connectionRefs);
|
|
230
270
|
setMissingConnectors(result.missingConnectors);
|
|
271
|
+
setMissingConnectionReferences(result.missingConnectionReferences);
|
|
272
|
+
setConnectionRequirements(result.connectionRequirements);
|
|
273
|
+
setResourceConsents(result.resourceConsents);
|
|
274
|
+
setConfigurationErrors(result.configurationErrors);
|
|
275
|
+
setRequiresConnectionConsent(result.requiresConnectionConsent);
|
|
231
276
|
setDataverseOrgUrl(result.dataverseOrgUrl);
|
|
277
|
+
setDatabaseReferences(result.databaseReferences);
|
|
278
|
+
setResolvedConfigKey(currentConfigKey);
|
|
232
279
|
setIsReady(true);
|
|
280
|
+
resolutionScenario.complete({
|
|
281
|
+
result: 'resolved',
|
|
282
|
+
missingConnectionReferenceCount: result.missingConnectionReferences.length,
|
|
283
|
+
configurationErrorCount: result.configurationErrors.length
|
|
284
|
+
});
|
|
233
285
|
}
|
|
234
286
|
}
|
|
235
287
|
loadConnections().catch(e => {
|
|
@@ -240,6 +292,9 @@ export function useConnectionRefs({
|
|
|
240
292
|
// production without raising a __DEV__ LogBox redbox over the
|
|
241
293
|
// otherwise-working UI. We still recover via setIsReady below.
|
|
242
294
|
debugLog('[useConnectionRefs] Failed to load connections:', e);
|
|
295
|
+
resolutionScenario.fail(undefined, {
|
|
296
|
+
errorName: e instanceof Error ? e.name : 'UnknownError'
|
|
297
|
+
});
|
|
243
298
|
getAppLogger().trackErrorEvent(HostOperationNames.PlatformLoadConnectionsFailed, {
|
|
244
299
|
error: String(e),
|
|
245
300
|
hasAppId: Boolean(appId),
|
|
@@ -247,7 +302,9 @@ export function useConnectionRefs({
|
|
|
247
302
|
connectionReferenceCount: Object.keys(connectionReferences).length,
|
|
248
303
|
runningInDevPlayer: isDevPlayer
|
|
249
304
|
});
|
|
250
|
-
|
|
305
|
+
const accessDenied = e instanceof AppLaunchAccessDeniedError;
|
|
306
|
+
setErrorKind(accessDenied ? 'app-access-denied' : 'generic');
|
|
307
|
+
setError(accessDenied ? 'You don’t have access to this app. Ask the app owner or an administrator to share it with you, then try again.' : 'Unable to load this app from Power Platform. Try again later.');
|
|
251
308
|
setIsReady(true);
|
|
252
309
|
}
|
|
253
310
|
});
|
|
@@ -256,12 +313,20 @@ export function useConnectionRefs({
|
|
|
256
313
|
if (retryTimer) clearTimeout(retryTimer);
|
|
257
314
|
};
|
|
258
315
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
259
|
-
}, [isSignedIn, retryTick]);
|
|
316
|
+
}, [isSignedIn, retryTick, appId, environmentId, region, connectionReferences, hasDataverse, schemaMap, isvClientId]);
|
|
260
317
|
return {
|
|
261
318
|
connectionRefs,
|
|
262
319
|
isReady,
|
|
263
320
|
error,
|
|
321
|
+
errorKind,
|
|
264
322
|
missingConnectors,
|
|
265
|
-
|
|
323
|
+
missingConnectionReferences,
|
|
324
|
+
connectionRequirements,
|
|
325
|
+
resourceConsents,
|
|
326
|
+
configurationErrors,
|
|
327
|
+
requiresConnectionConsent,
|
|
328
|
+
resolvedConfigKey,
|
|
329
|
+
dataverseOrgUrl,
|
|
330
|
+
databaseReferences
|
|
266
331
|
};
|
|
267
332
|
}
|