@microsoft/power-apps-native-host 0.2.22 → 0.3.0

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 (156) hide show
  1. package/README.md +162 -26
  2. package/compatibility/current.json +87 -0
  3. package/lib/NOTICE.txt +140 -1226
  4. package/lib/commonjs/auth/AuthContext.js +74 -18
  5. package/lib/commonjs/auth/AuthContext.web.js +32 -0
  6. package/lib/commonjs/auth/completePowerAppsAuthSession.js +16 -0
  7. package/lib/commonjs/bridge/NativePowerAppsBridge.js +20 -17
  8. package/lib/commonjs/bridge/PowerPagesBridge.js +157 -0
  9. package/lib/commonjs/bridge/bridgeState.js +40 -0
  10. package/lib/commonjs/connections/ConnectionSetupScreen.js +459 -80
  11. package/lib/commonjs/connections/PowerPlatformConnectionApi.js +946 -0
  12. package/lib/commonjs/connections/connectionMetadata.js +127 -0
  13. package/lib/commonjs/connections/connectionState.js +96 -0
  14. package/lib/commonjs/connections/disambiguate.js +19 -11
  15. package/lib/commonjs/connections/resolveConnections.js +347 -0
  16. package/lib/commonjs/connections/types.js +106 -0
  17. package/lib/commonjs/{platform → connections}/useConnectionRefs.js +128 -65
  18. package/lib/commonjs/connections/useConnectionSetup.js +111 -52
  19. package/lib/commonjs/controls/FilePicker.js +6 -0
  20. package/lib/commonjs/controls/ImagePicker.js +8 -0
  21. package/lib/commonjs/index.js +188 -6
  22. package/lib/commonjs/logger.js +9 -6
  23. package/lib/commonjs/offline/createOfflineHostActivationCallbacks.js +61 -0
  24. package/lib/commonjs/offline/useOfflineIntegration.js +12 -0
  25. package/lib/commonjs/offline/useOfflineIntegration.web.js +11 -0
  26. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.js +46 -0
  27. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.web.js +13 -0
  28. package/lib/commonjs/platform/DataverseDataSources.js +14 -0
  29. package/lib/commonjs/platform/DataverseGlobalDiscoveryApi.js +49 -0
  30. package/lib/commonjs/platform/GlobalDiscovery.js +51 -0
  31. package/lib/commonjs/platform/PowerPlatformEnvironmentApi.js +6 -106
  32. package/lib/commonjs/provider/PowerAppsHostProvider.js +156 -25
  33. package/lib/commonjs/provider/PowerAppsHostProvider.web.js +41 -0
  34. package/lib/commonjs/provider/PowerAppsProvider.js +229 -133
  35. package/lib/commonjs/provider/SessionDetailsGesture.js +216 -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 +74 -18
  40. package/lib/module/auth/AuthContext.web.js +26 -0
  41. package/lib/module/auth/completePowerAppsAuthSession.js +11 -0
  42. package/lib/module/bridge/NativePowerAppsBridge.js +21 -18
  43. package/lib/module/bridge/PowerPagesBridge.js +151 -0
  44. package/lib/module/bridge/bridgeState.js +34 -0
  45. package/lib/module/connections/ConnectionSetupScreen.js +460 -81
  46. package/lib/module/connections/PowerPlatformConnectionApi.js +932 -0
  47. package/lib/module/connections/connectionMetadata.js +112 -0
  48. package/lib/module/connections/connectionState.js +82 -0
  49. package/lib/module/connections/disambiguate.js +19 -12
  50. package/lib/module/connections/resolveConnections.js +342 -0
  51. package/lib/module/connections/types.js +91 -0
  52. package/lib/module/{platform → connections}/useConnectionRefs.js +129 -66
  53. package/lib/module/connections/useConnectionSetup.js +113 -54
  54. package/lib/module/controls/FilePicker.js +6 -0
  55. package/lib/module/controls/ImagePicker.js +8 -0
  56. package/lib/module/index.js +14 -6
  57. package/lib/module/logger.js +8 -6
  58. package/lib/module/offline/createOfflineHostActivationCallbacks.js +55 -0
  59. package/lib/module/offline/useOfflineIntegration.js +3 -0
  60. package/lib/module/offline/useOfflineIntegration.web.js +7 -0
  61. package/lib/module/offline/useOfflineIntegrationWithStartupGate.js +42 -0
  62. package/lib/module/offline/useOfflineIntegrationWithStartupGate.web.js +9 -0
  63. package/lib/module/platform/DataverseDataSources.js +10 -0
  64. package/lib/module/platform/DataverseGlobalDiscoveryApi.js +45 -0
  65. package/lib/module/platform/GlobalDiscovery.js +45 -0
  66. package/lib/module/platform/PowerPlatformEnvironmentApi.js +5 -104
  67. package/lib/module/provider/PowerAppsHostProvider.js +159 -28
  68. package/lib/module/provider/PowerAppsHostProvider.web.js +36 -0
  69. package/lib/module/provider/PowerAppsProvider.js +233 -137
  70. package/lib/module/provider/SessionDetailsGesture.js +211 -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 +21 -0
  76. package/lib/typescript/commonjs/auth/completePowerAppsAuthSession.d.ts +8 -0
  77. package/lib/typescript/commonjs/bridge/PowerPagesBridge.d.ts +18 -0
  78. package/lib/typescript/commonjs/bridge/bridgeState.d.ts +5 -0
  79. package/lib/typescript/commonjs/connections/ConnectionSetupScreen.d.ts +28 -7
  80. package/lib/typescript/commonjs/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  81. package/lib/typescript/commonjs/connections/connectionMetadata.d.ts +66 -0
  82. package/lib/typescript/commonjs/connections/connectionState.d.ts +16 -0
  83. package/lib/typescript/commonjs/connections/disambiguate.d.ts +4 -2
  84. package/lib/typescript/commonjs/{platform → connections}/resolveConnections.d.ts +19 -19
  85. package/lib/typescript/commonjs/connections/types.d.ts +129 -1
  86. package/lib/typescript/commonjs/{platform → connections}/useConnectionRefs.d.ts +33 -24
  87. package/lib/typescript/commonjs/connections/useConnectionSetup.d.ts +12 -8
  88. package/lib/typescript/commonjs/controls/FilePicker.d.ts +7 -1
  89. package/lib/typescript/commonjs/controls/ImagePicker.d.ts +9 -1
  90. package/lib/typescript/commonjs/index.d.ts +20 -10
  91. package/lib/typescript/commonjs/logger.d.ts +4 -4
  92. package/lib/typescript/commonjs/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  93. package/lib/typescript/commonjs/offline/useOfflineIntegration.d.ts +2 -0
  94. package/lib/typescript/commonjs/offline/useOfflineIntegration.web.d.ts +4 -0
  95. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  96. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  97. package/lib/typescript/commonjs/platform/DataverseDataSources.d.ts +27 -0
  98. package/lib/typescript/commonjs/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  99. package/lib/typescript/commonjs/platform/GlobalDiscovery.d.ts +4 -0
  100. package/lib/typescript/commonjs/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  101. package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +29 -18
  102. package/lib/typescript/commonjs/provider/PowerAppsHostProvider.web.d.ts +5 -0
  103. package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +32 -10
  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 +21 -0
  110. package/lib/typescript/module/auth/completePowerAppsAuthSession.d.ts +8 -0
  111. package/lib/typescript/module/bridge/PowerPagesBridge.d.ts +18 -0
  112. package/lib/typescript/module/bridge/bridgeState.d.ts +5 -0
  113. package/lib/typescript/module/connections/ConnectionSetupScreen.d.ts +28 -7
  114. package/lib/typescript/module/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  115. package/lib/typescript/module/connections/connectionMetadata.d.ts +66 -0
  116. package/lib/typescript/module/connections/connectionState.d.ts +16 -0
  117. package/lib/typescript/module/connections/disambiguate.d.ts +4 -2
  118. package/lib/typescript/module/{platform → connections}/resolveConnections.d.ts +19 -19
  119. package/lib/typescript/module/connections/types.d.ts +129 -1
  120. package/lib/typescript/module/{platform → connections}/useConnectionRefs.d.ts +33 -24
  121. package/lib/typescript/module/connections/useConnectionSetup.d.ts +12 -8
  122. package/lib/typescript/module/controls/FilePicker.d.ts +7 -1
  123. package/lib/typescript/module/controls/ImagePicker.d.ts +9 -1
  124. package/lib/typescript/module/index.d.ts +24 -14
  125. package/lib/typescript/module/logger.d.ts +4 -4
  126. package/lib/typescript/module/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  127. package/lib/typescript/module/offline/useOfflineIntegration.d.ts +2 -0
  128. package/lib/typescript/module/offline/useOfflineIntegration.web.d.ts +4 -0
  129. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  130. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  131. package/lib/typescript/module/platform/DataverseDataSources.d.ts +27 -0
  132. package/lib/typescript/module/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  133. package/lib/typescript/module/platform/GlobalDiscovery.d.ts +4 -0
  134. package/lib/typescript/module/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  135. package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +29 -18
  136. package/lib/typescript/module/provider/PowerAppsHostProvider.web.d.ts +5 -0
  137. package/lib/typescript/module/provider/PowerAppsProvider.d.ts +34 -12
  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 +43 -18
  143. package/scripts/build-ios-base.js +66 -27
  144. package/scripts/bundle.js +212 -5
  145. package/scripts/generate-connector-schemas.js +8 -6
  146. package/scripts/metro-logger.js +213 -0
  147. package/scripts/start-web.mjs +137 -0
  148. package/scripts/upgrade/runtime.js +463 -0
  149. package/scripts/upgrade/system.js +244 -0
  150. package/scripts/upgrade-authoring.js +534 -0
  151. package/scripts/upgrade.js +188 -0
  152. package/scripts/wrap.js +536 -0
  153. package/lib/commonjs/platform/PowerPlatformConnectionApi.js +0 -606
  154. package/lib/commonjs/platform/resolveConnections.js +0 -264
  155. package/lib/module/platform/PowerPlatformConnectionApi.js +0 -595
  156. 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,
@@ -78,31 +72,37 @@ export function useConnectionRefs({
78
72
  const [connectionRefs, setConnectionRefs] = useState({});
79
73
  const [isReady, setIsReady] = useState(false);
80
74
  const [error, setError] = useState(null);
75
+ const [errorKind, setErrorKind] = useState(null);
81
76
  const [missingConnectors, setMissingConnectors] = useState([]);
77
+ const [missingConnectionReferences, setMissingConnectionReferences] = useState([]);
78
+ const [connectionRequirements, setConnectionRequirements] = useState([]);
79
+ const [resourceConsents, setResourceConsents] = useState([]);
80
+ const [configurationErrors, setConfigurationErrors] = useState([]);
81
+ const [requiresConnectionConsent, setRequiresConnectionConsent] = useState(false);
82
+ const [resolvedConfigKey, setResolvedConfigKey] = useState(null);
82
83
  const [dataverseOrgUrl, setDataverseOrgUrl] = useState('');
84
+ const [databaseReferences, setDatabaseReferences] = useState({});
83
85
 
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.
86
+ // Bounded re-resolution for transient silent-token misses.
90
87
  const [retryTick, setRetryTick] = useState(0);
91
88
  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.
89
+ // Dev Player retries in the background without dropping its locally ready UI.
97
90
  const isBackgroundRetryRef = useRef(false);
98
91
  const MAX_TOKEN_RETRIES = 5;
99
92
  const TOKEN_RETRY_DELAY_MS = 2000;
93
+ const currentConfigKey = buildConnectionResolutionKey(appId, environmentId, isDevPlayer ? connectionReferences : {});
100
94
  useEffect(() => {
101
95
  if (!isSignedIn) {
102
96
  tokenRetryCountRef.current = 0;
103
97
  isBackgroundRetryRef.current = false;
104
98
  return;
105
99
  }
100
+ if (!isDevPlayer && (!appId || !environmentId)) {
101
+ setErrorKind('generic');
102
+ setError('The production app requires both an app ID and an environment ID before it can load from Power Platform.');
103
+ setIsReady(true);
104
+ return;
105
+ }
106
106
 
107
107
  // Consume the background-retry flag once per run so a later initial run or
108
108
  // sign-in can never inherit a stale value.
@@ -120,6 +120,7 @@ export function useConnectionRefs({
120
120
  // an online (offline-disabled) played app never resolves its instanceUrl in
121
121
  // the Dev Player and every Dataverse read fails with "No instanceUrl found".
122
122
  if (isDevPlayer && !hasConnectionRefs && !environmentId) {
123
+ setResolvedConfigKey(currentConfigKey);
123
124
  setIsReady(true);
124
125
  return;
125
126
  }
@@ -128,50 +129,63 @@ export function useConnectionRefs({
128
129
  // (and no appId to fetch from the launch API).
129
130
  // Mark ready immediately so the bridge can initialise without any network calls.
130
131
  if (!environmentId && !appId) {
132
+ setResolvedConfigKey(currentConfigKey);
131
133
  setIsReady(true);
132
134
  return;
133
135
  }
134
136
  let cancelled = false;
135
137
  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.
138
+ const resolutionScenario = getAppLogger().trackScenario(HostOperationNames.PlatformResolveConnectionRefs, {
139
+ mode: isDevPlayer ? 'devPlayer' : 'publishedApp',
140
+ hasAppId: Boolean(appId),
141
+ connectionReferenceCount: Object.keys(connectionReferences).length
142
+ });
143
+ // Preserve the locally ready Dev Player UI during a background retry.
140
144
  if (!isBackgroundRetry) {
141
145
  setIsReady(false);
142
146
  setError(null);
147
+ setErrorKind(null);
143
148
  setMissingConnectors([]);
149
+ setMissingConnectionReferences([]);
150
+ setConnectionRequirements([]);
151
+ setResourceConsents([]);
152
+ setConfigurationErrors([]);
153
+ setRequiresConnectionConsent(false);
154
+ setResolvedConfigKey(null);
155
+ setDataverseOrgUrl('');
156
+ setDatabaseReferences({});
144
157
  }
145
158
  async function loadConnections() {
146
159
  const token = await acquireToken([powerPlatformScope(region)]);
147
- if (cancelled) return;
160
+ if (cancelled) {
161
+ resolutionScenario.complete({
162
+ result: 'cancelled'
163
+ });
164
+ return;
165
+ }
148
166
  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.
167
+ resolutionScenario.completeWithError({
168
+ reason: 'tokenUnavailable'
169
+ });
170
+ // Dev Player can render local metadata while token recovery continues;
171
+ // standalone apps stay blocked because production has no static fallback.
172
+ if (isDevPlayer) {
173
+ setConnectionRefs(buildConnectionReferences(connectionReferences));
174
+ setIsReady(true);
175
+ setResolvedConfigKey(currentConfigKey);
176
+ }
167
177
  if (tokenRetryCountRef.current < MAX_TOKEN_RETRIES) {
168
178
  tokenRetryCountRef.current += 1;
169
179
  retryTimer = setTimeout(() => {
170
180
  if (!cancelled) {
171
- isBackgroundRetryRef.current = true;
181
+ isBackgroundRetryRef.current = isDevPlayer;
172
182
  setRetryTick(t => t + 1);
173
183
  }
174
184
  }, TOKEN_RETRY_DELAY_MS);
185
+ } else if (!isDevPlayer) {
186
+ setErrorKind('generic');
187
+ setError('Unable to authenticate with Power Platform to load this app. Sign in again and retry.');
188
+ setIsReady(true);
175
189
  }
176
190
  return;
177
191
  }
@@ -185,28 +199,47 @@ export function useConnectionRefs({
185
199
  let result = null;
186
200
  if (appId && environmentId && !isDevPlayer) {
187
201
  result = await fetchAppLaunchData(appId, environmentId, region, token);
202
+ if (!result) {
203
+ throw new Error(`App "${appId}" is not available in environment "${environmentId}". Push the mobile app to this environment and try again.`);
204
+ }
205
+ if (result && onDisambiguationNeeded) {
206
+ for (const requirement of [...result.missingConnectionReferences]) {
207
+ const candidates = result.connectionCandidates[requirement.localRefId] ?? [];
208
+ if (candidates.length < 2) continue;
209
+ const selectedId = await onDisambiguationNeeded({
210
+ localRefId: requirement.localRefId,
211
+ apiId: requirement.apiId,
212
+ candidates
213
+ });
214
+ if (selectedId) {
215
+ selectLaunchConnection(result, requirement.localRefId, selectedId);
216
+ }
217
+ }
218
+ }
188
219
  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');
220
+ const failedConnectionNames = await applyAclGrants(result.connectionRefs, result.connectionRequirements, environmentId, region, acquireToken);
221
+ invalidateFailedAclGrants(result, failedConnectionNames);
222
+ } else if (result) {
223
+ debugWarn('[useConnectionRefs] could not grant user-selected connections; treating them as setup-required');
192
224
  getAppLogger().trackErrorEvent(HostOperationNames.PlatformConnectionAclGrant, {
193
225
  phase: 'launchAclVerify',
194
- reason: result ? 'missingIsvClientId' : 'launchDataUnavailable',
195
- connectionRefCount: result ? Object.keys(result.connectionRefs).length : 0
226
+ reason: 'missingIsvClientId',
227
+ connectionRefCount: Object.keys(result.connectionRefs).length
196
228
  });
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
- }
229
+ const unresolved = new Set(getConnectionNamesRequiringAclGrant(result.connectionRefs, result.connectionRequirements));
230
+ invalidateFailedAclGrants(result, unresolved);
231
+ }
232
+ if (result) {
233
+ applyRegisteredConnectionUsages(result, result.registeredConnectionUsages);
204
234
  }
205
235
  }
206
236
 
207
237
  // Slow path: per-connector resolution.
208
- // Used when: running in Dev Player, no appId (local dev), or launch API failed.
238
+ // Used only in Dev Player, where unpublished local app metadata is expected.
209
239
  if (!result) {
240
+ if (!isDevPlayer) {
241
+ throw new Error('Unable to load the production app from the Power Platform launch endpoint.');
242
+ }
210
243
  result = await resolveViaStaticRefs({
211
244
  connectionReferences,
212
245
  schemaMap,
@@ -219,7 +252,12 @@ export function useConnectionRefs({
219
252
  onMissingConnection
220
253
  });
221
254
  }
222
- if (cancelled) return;
255
+ if (cancelled) {
256
+ resolutionScenario.complete({
257
+ result: 'cancelled'
258
+ });
259
+ return;
260
+ }
223
261
 
224
262
  // Pre-warm the Dataverse token while the user waits.
225
263
  if (result.dataverseOrgUrl) {
@@ -228,8 +266,20 @@ export function useConnectionRefs({
228
266
  if (!cancelled) {
229
267
  setConnectionRefs(result.connectionRefs);
230
268
  setMissingConnectors(result.missingConnectors);
269
+ setMissingConnectionReferences(result.missingConnectionReferences);
270
+ setConnectionRequirements(result.connectionRequirements);
271
+ setResourceConsents(result.resourceConsents);
272
+ setConfigurationErrors(result.configurationErrors);
273
+ setRequiresConnectionConsent(result.requiresConnectionConsent);
231
274
  setDataverseOrgUrl(result.dataverseOrgUrl);
275
+ setDatabaseReferences(result.databaseReferences);
276
+ setResolvedConfigKey(currentConfigKey);
232
277
  setIsReady(true);
278
+ resolutionScenario.complete({
279
+ result: 'resolved',
280
+ missingConnectionReferenceCount: result.missingConnectionReferences.length,
281
+ configurationErrorCount: result.configurationErrors.length
282
+ });
233
283
  }
234
284
  }
235
285
  loadConnections().catch(e => {
@@ -240,6 +290,9 @@ export function useConnectionRefs({
240
290
  // production without raising a __DEV__ LogBox redbox over the
241
291
  // otherwise-working UI. We still recover via setIsReady below.
242
292
  debugLog('[useConnectionRefs] Failed to load connections:', e);
293
+ resolutionScenario.fail(undefined, {
294
+ errorName: e instanceof Error ? e.name : 'UnknownError'
295
+ });
243
296
  getAppLogger().trackErrorEvent(HostOperationNames.PlatformLoadConnectionsFailed, {
244
297
  error: String(e),
245
298
  hasAppId: Boolean(appId),
@@ -247,7 +300,9 @@ export function useConnectionRefs({
247
300
  connectionReferenceCount: Object.keys(connectionReferences).length,
248
301
  runningInDevPlayer: isDevPlayer
249
302
  });
250
- setError(String(e));
303
+ const accessDenied = e instanceof AppLaunchAccessDeniedError;
304
+ setErrorKind(accessDenied ? 'app-access-denied' : 'generic');
305
+ 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
306
  setIsReady(true);
252
307
  }
253
308
  });
@@ -256,12 +311,20 @@ export function useConnectionRefs({
256
311
  if (retryTimer) clearTimeout(retryTimer);
257
312
  };
258
313
  // eslint-disable-next-line react-hooks/exhaustive-deps
259
- }, [isSignedIn, retryTick]);
314
+ }, [isSignedIn, retryTick, appId, environmentId, region, connectionReferences, schemaMap, isvClientId]);
260
315
  return {
261
316
  connectionRefs,
262
317
  isReady,
263
318
  error,
319
+ errorKind,
264
320
  missingConnectors,
265
- dataverseOrgUrl
321
+ missingConnectionReferences,
322
+ connectionRequirements,
323
+ resourceConsents,
324
+ configurationErrors,
325
+ requiresConnectionConsent,
326
+ resolvedConfigKey,
327
+ dataverseOrgUrl,
328
+ databaseReferences
266
329
  };
267
330
  }