@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.
Files changed (154) hide show
  1. package/README.md +21 -10
  2. package/compatibility/current.json +31 -4
  3. package/lib/NOTICE.txt +1325 -24
  4. package/lib/commonjs/auth/AuthContext.js +110 -33
  5. package/lib/commonjs/bridge/NativePowerAppsBridge.js +11 -18
  6. package/lib/commonjs/bridge/PowerPagesBridge.js +27 -8
  7. package/lib/commonjs/bridge/bridgeState.js +40 -0
  8. package/lib/commonjs/config/babelConfig.js +13 -0
  9. package/lib/commonjs/config/expoConfig.js +71 -0
  10. package/lib/commonjs/config/metroConfig.js +70 -0
  11. package/lib/commonjs/config/metroLogger.js +182 -0
  12. package/lib/commonjs/config/tamaguiConfig.js +98 -0
  13. package/lib/commonjs/connections/ConnectionSetupScreen.js +459 -80
  14. package/lib/commonjs/connections/PowerPlatformConnectionApi.js +946 -0
  15. package/lib/commonjs/connections/connectionMetadata.js +127 -0
  16. package/lib/commonjs/connections/connectionState.js +96 -0
  17. package/lib/commonjs/connections/disambiguate.js +19 -11
  18. package/lib/commonjs/connections/resolveConnections.js +348 -0
  19. package/lib/commonjs/connections/types.js +106 -0
  20. package/lib/commonjs/{platform → connections}/useConnectionRefs.js +130 -65
  21. package/lib/commonjs/connections/useConnectionSetup.js +111 -52
  22. package/lib/commonjs/controls/FilePicker.js +6 -0
  23. package/lib/commonjs/controls/ImagePicker.js +8 -0
  24. package/lib/commonjs/index.js +167 -4
  25. package/lib/commonjs/logger.js +9 -6
  26. package/lib/commonjs/offline/createOfflineHostActivationCallbacks.js +61 -0
  27. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.js +48 -0
  28. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.web.js +13 -0
  29. package/lib/commonjs/platform/DataverseDataSources.js +18 -0
  30. package/lib/commonjs/platform/DataverseGlobalDiscoveryApi.js +94 -0
  31. package/lib/commonjs/platform/GlobalDiscovery.js +51 -0
  32. package/lib/commonjs/platform/PowerPlatformEnvironmentApi.js +6 -106
  33. package/lib/commonjs/provider/PowerAppsHostProvider.js +157 -25
  34. package/lib/commonjs/provider/PowerAppsProvider.js +178 -134
  35. package/lib/commonjs/provider/SessionDetailsGesture.js +208 -0
  36. package/lib/commonjs/telemetry/OfflineSdkTelemetry.js +95 -0
  37. package/lib/commonjs/telemetry/OfflineSdkTelemetry.web.js +9 -0
  38. package/lib/commonjs/telemetry/index.js +22 -3
  39. package/lib/module/auth/AuthContext.js +110 -33
  40. package/lib/module/bridge/NativePowerAppsBridge.js +12 -19
  41. package/lib/module/bridge/PowerPagesBridge.js +27 -8
  42. package/lib/module/bridge/bridgeState.js +34 -0
  43. package/lib/module/config/babelConfig.js +9 -0
  44. package/lib/module/config/expoConfig.js +67 -0
  45. package/lib/module/config/metroConfig.js +65 -0
  46. package/lib/module/config/metroLogger.js +175 -0
  47. package/lib/module/config/tamaguiConfig.js +93 -0
  48. package/lib/module/connections/ConnectionSetupScreen.js +460 -81
  49. package/lib/module/connections/PowerPlatformConnectionApi.js +932 -0
  50. package/lib/module/connections/connectionMetadata.js +112 -0
  51. package/lib/module/connections/connectionState.js +82 -0
  52. package/lib/module/connections/disambiguate.js +19 -12
  53. package/lib/module/connections/resolveConnections.js +343 -0
  54. package/lib/module/connections/types.js +91 -0
  55. package/lib/module/{platform → connections}/useConnectionRefs.js +131 -66
  56. package/lib/module/connections/useConnectionSetup.js +113 -54
  57. package/lib/module/controls/FilePicker.js +6 -0
  58. package/lib/module/controls/ImagePicker.js +8 -0
  59. package/lib/module/index.js +14 -4
  60. package/lib/module/logger.js +8 -6
  61. package/lib/module/offline/createOfflineHostActivationCallbacks.js +55 -0
  62. package/lib/module/offline/useOfflineIntegrationWithStartupGate.js +44 -0
  63. package/lib/module/offline/useOfflineIntegrationWithStartupGate.web.js +9 -0
  64. package/lib/module/platform/DataverseDataSources.js +13 -0
  65. package/lib/module/platform/DataverseGlobalDiscoveryApi.js +89 -0
  66. package/lib/module/platform/GlobalDiscovery.js +45 -0
  67. package/lib/module/platform/PowerPlatformEnvironmentApi.js +5 -104
  68. package/lib/module/provider/PowerAppsHostProvider.js +160 -28
  69. package/lib/module/provider/PowerAppsProvider.js +182 -138
  70. package/lib/module/provider/SessionDetailsGesture.js +203 -0
  71. package/lib/module/telemetry/OfflineSdkTelemetry.js +91 -0
  72. package/lib/module/telemetry/OfflineSdkTelemetry.web.js +5 -0
  73. package/lib/module/telemetry/index.js +15 -4
  74. package/lib/typescript/commonjs/auth/AuthContext.d.ts +3 -1
  75. package/lib/typescript/commonjs/auth/AuthContext.web.d.ts +1 -0
  76. package/lib/typescript/commonjs/bridge/bridgeState.d.ts +5 -0
  77. package/lib/typescript/commonjs/config/babelConfig.d.ts +12 -0
  78. package/lib/typescript/commonjs/config/expoConfig.d.ts +21 -0
  79. package/lib/typescript/commonjs/config/metroConfig.d.ts +6 -0
  80. package/lib/typescript/commonjs/config/metroLogger.d.ts +21 -0
  81. package/lib/typescript/commonjs/config/tamaguiConfig.d.ts +1792 -0
  82. package/lib/typescript/commonjs/connections/ConnectionSetupScreen.d.ts +28 -7
  83. package/lib/typescript/commonjs/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  84. package/lib/typescript/commonjs/connections/connectionMetadata.d.ts +66 -0
  85. package/lib/typescript/commonjs/connections/connectionState.d.ts +16 -0
  86. package/lib/typescript/commonjs/connections/disambiguate.d.ts +4 -2
  87. package/lib/typescript/commonjs/{platform → connections}/resolveConnections.d.ts +22 -20
  88. package/lib/typescript/commonjs/connections/types.d.ts +129 -1
  89. package/lib/typescript/commonjs/{platform → connections}/useConnectionRefs.d.ts +36 -25
  90. package/lib/typescript/commonjs/connections/useConnectionSetup.d.ts +12 -8
  91. package/lib/typescript/commonjs/controls/FilePicker.d.ts +7 -1
  92. package/lib/typescript/commonjs/controls/ImagePicker.d.ts +9 -1
  93. package/lib/typescript/commonjs/index.d.ts +18 -10
  94. package/lib/typescript/commonjs/logger.d.ts +4 -4
  95. package/lib/typescript/commonjs/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  96. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  97. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  98. package/lib/typescript/commonjs/platform/DataverseDataSources.d.ts +28 -0
  99. package/lib/typescript/commonjs/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  100. package/lib/typescript/commonjs/platform/GlobalDiscovery.d.ts +4 -0
  101. package/lib/typescript/commonjs/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  102. package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +29 -18
  103. package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +54 -9
  104. package/lib/typescript/commonjs/provider/SessionDetailsGesture.d.ts +13 -0
  105. package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.d.ts +15 -0
  106. package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
  107. package/lib/typescript/commonjs/telemetry/index.d.ts +13 -1
  108. package/lib/typescript/module/auth/AuthContext.d.ts +3 -1
  109. package/lib/typescript/module/auth/AuthContext.web.d.ts +1 -0
  110. package/lib/typescript/module/bridge/bridgeState.d.ts +5 -0
  111. package/lib/typescript/module/config/babelConfig.d.ts +12 -0
  112. package/lib/typescript/module/config/expoConfig.d.ts +21 -0
  113. package/lib/typescript/module/config/metroConfig.d.ts +6 -0
  114. package/lib/typescript/module/config/metroLogger.d.ts +21 -0
  115. package/lib/typescript/module/config/tamaguiConfig.d.ts +1792 -0
  116. package/lib/typescript/module/connections/ConnectionSetupScreen.d.ts +28 -7
  117. package/lib/typescript/module/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  118. package/lib/typescript/module/connections/connectionMetadata.d.ts +66 -0
  119. package/lib/typescript/module/connections/connectionState.d.ts +16 -0
  120. package/lib/typescript/module/connections/disambiguate.d.ts +4 -2
  121. package/lib/typescript/module/{platform → connections}/resolveConnections.d.ts +22 -20
  122. package/lib/typescript/module/connections/types.d.ts +129 -1
  123. package/lib/typescript/module/{platform → connections}/useConnectionRefs.d.ts +36 -25
  124. package/lib/typescript/module/connections/useConnectionSetup.d.ts +12 -8
  125. package/lib/typescript/module/controls/FilePicker.d.ts +7 -1
  126. package/lib/typescript/module/controls/ImagePicker.d.ts +9 -1
  127. package/lib/typescript/module/index.d.ts +18 -10
  128. package/lib/typescript/module/logger.d.ts +4 -4
  129. package/lib/typescript/module/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  130. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  131. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  132. package/lib/typescript/module/platform/DataverseDataSources.d.ts +28 -0
  133. package/lib/typescript/module/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  134. package/lib/typescript/module/platform/GlobalDiscovery.d.ts +4 -0
  135. package/lib/typescript/module/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  136. package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +29 -18
  137. package/lib/typescript/module/provider/PowerAppsProvider.d.ts +54 -9
  138. package/lib/typescript/module/provider/SessionDetailsGesture.d.ts +13 -0
  139. package/lib/typescript/module/telemetry/OfflineSdkTelemetry.d.ts +15 -0
  140. package/lib/typescript/module/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
  141. package/lib/typescript/module/telemetry/index.d.ts +13 -1
  142. package/package.json +52 -13
  143. package/scripts/build-codegen-package.js +283 -0
  144. package/scripts/start-web.mjs +9 -5
  145. package/scripts/upgrade/runtime.js +139 -24
  146. package/scripts/upgrade-authoring.js +274 -180
  147. package/scripts/upgrade.js +10 -16
  148. package/src/config/generated-config.d.ts +4 -0
  149. package/src/config/index.d.ts +3 -0
  150. package/src/config/tsconfig.json +26 -0
  151. package/lib/commonjs/platform/PowerPlatformConnectionApi.js +0 -606
  152. package/lib/commonjs/platform/resolveConnections.js +0 -264
  153. package/lib/module/platform/PowerPlatformConnectionApi.js +0 -595
  154. 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 "./PowerPlatformEnvironmentApi.js";
39
- import { ConnectionStatus } from "../connections/types.js";
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
- * On sign-in this hook:
55
- * 1. If `appId` is set (production template), fetches connection references
56
- * from the PowerApps RP `/apps/{appId}/launch` endpoint and uses those
57
- * in place of the static `connectionReferences` from power.config.json.
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 the transient null-token case. A null token at
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
- // True only when this effect run was triggered by the bounded background
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
- // Skip the reset on a background-retry run: the UI is already mounted with
137
- // static fallback refs, so re-resolution must happen in the background. The
138
- // full-resolution path below still sets missingConnectors/error from the
139
- // real result. Resetting here would drop isReady → unmount the subtree.
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) return;
161
+ if (cancelled) {
162
+ resolutionScenario.complete({
163
+ result: 'cancelled'
164
+ });
165
+ return;
166
+ }
148
167
  if (!token) {
149
- // Cold boot before an auth token is available (e.g. silent-token miss or
150
- // offline launch). The per-user runtimeUrl/connectionName can't be
151
- // resolved yet, but seed the bridge with whatever static apiId entries can
152
- // be derived from connectionReferences so the runtime fields populate on
153
- // the next resolution. NOTE: when connectionReferences is empty (the
154
- // appId-only fast path) this seed is itself empty, so the bridge starts
155
- // with no connections until the bounded retry below resolves a token.
156
- // That gap is transient as long as a retry succeeds within
157
- // MAX_TOKEN_RETRIES; if the budget is exhausted the bridge stays seeded
158
- // with the empty map until the next sign-out/in resets the retry count.
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 = true;
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, isvClientId, acquireToken);
190
- } else {
191
- debugWarn('[useConnectionRefs] could not verify connection ACLs; treating existing connections as setup-required');
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: result ? 'missingIsvClientId' : 'launchDataUnavailable',
195
- connectionRefCount: result ? Object.keys(result.connectionRefs).length : 0
227
+ reason: 'missingIsvClientId',
228
+ connectionRefCount: Object.keys(result.connectionRefs).length
196
229
  });
197
- for (const r of result ? Object.values(result.connectionRefs) : []) {
198
- for (const inst of r.instances || []) {
199
- if (r.status === ConnectionStatus.Connected) {
200
- r.status = ConnectionStatus.NeedsAclGrant;
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 when: running in Dev Player, no appId (local dev), or launch API failed.
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) return;
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
- setError(String(e));
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
- dataverseOrgUrl
323
+ missingConnectionReferences,
324
+ connectionRequirements,
325
+ resourceConsents,
326
+ configurationErrors,
327
+ requiresConnectionConsent,
328
+ resolvedConfigKey,
329
+ dataverseOrgUrl,
330
+ databaseReferences
266
331
  };
267
332
  }