@microsoft/power-apps-native-host 0.2.25 → 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 (129) hide show
  1. package/README.md +21 -2
  2. package/compatibility/current.json +0 -1
  3. package/lib/NOTICE.txt +6 -12
  4. package/lib/commonjs/auth/AuthContext.js +74 -18
  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/connections/ConnectionSetupScreen.js +459 -80
  9. package/lib/commonjs/connections/PowerPlatformConnectionApi.js +946 -0
  10. package/lib/commonjs/connections/connectionMetadata.js +127 -0
  11. package/lib/commonjs/connections/connectionState.js +96 -0
  12. package/lib/commonjs/connections/disambiguate.js +19 -11
  13. package/lib/commonjs/connections/resolveConnections.js +347 -0
  14. package/lib/commonjs/connections/types.js +106 -0
  15. package/lib/commonjs/{platform → connections}/useConnectionRefs.js +128 -65
  16. package/lib/commonjs/connections/useConnectionSetup.js +111 -52
  17. package/lib/commonjs/controls/FilePicker.js +6 -0
  18. package/lib/commonjs/controls/ImagePicker.js +8 -0
  19. package/lib/commonjs/index.js +166 -4
  20. package/lib/commonjs/logger.js +9 -6
  21. package/lib/commonjs/offline/createOfflineHostActivationCallbacks.js +61 -0
  22. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.js +46 -0
  23. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.web.js +13 -0
  24. package/lib/commonjs/platform/DataverseDataSources.js +14 -0
  25. package/lib/commonjs/platform/DataverseGlobalDiscoveryApi.js +49 -0
  26. package/lib/commonjs/platform/GlobalDiscovery.js +51 -0
  27. package/lib/commonjs/platform/PowerPlatformEnvironmentApi.js +6 -106
  28. package/lib/commonjs/provider/PowerAppsHostProvider.js +156 -25
  29. package/lib/commonjs/provider/PowerAppsProvider.js +173 -134
  30. package/lib/commonjs/provider/SessionDetailsGesture.js +216 -0
  31. package/lib/commonjs/telemetry/OfflineSdkTelemetry.js +95 -0
  32. package/lib/commonjs/telemetry/OfflineSdkTelemetry.web.js +9 -0
  33. package/lib/commonjs/telemetry/index.js +22 -3
  34. package/lib/module/auth/AuthContext.js +74 -18
  35. package/lib/module/bridge/NativePowerAppsBridge.js +12 -19
  36. package/lib/module/bridge/PowerPagesBridge.js +27 -8
  37. package/lib/module/bridge/bridgeState.js +34 -0
  38. package/lib/module/connections/ConnectionSetupScreen.js +460 -81
  39. package/lib/module/connections/PowerPlatformConnectionApi.js +932 -0
  40. package/lib/module/connections/connectionMetadata.js +112 -0
  41. package/lib/module/connections/connectionState.js +82 -0
  42. package/lib/module/connections/disambiguate.js +19 -12
  43. package/lib/module/connections/resolveConnections.js +342 -0
  44. package/lib/module/connections/types.js +91 -0
  45. package/lib/module/{platform → connections}/useConnectionRefs.js +129 -66
  46. package/lib/module/connections/useConnectionSetup.js +113 -54
  47. package/lib/module/controls/FilePicker.js +6 -0
  48. package/lib/module/controls/ImagePicker.js +8 -0
  49. package/lib/module/index.js +10 -4
  50. package/lib/module/logger.js +8 -6
  51. package/lib/module/offline/createOfflineHostActivationCallbacks.js +55 -0
  52. package/lib/module/offline/useOfflineIntegrationWithStartupGate.js +42 -0
  53. package/lib/module/offline/useOfflineIntegrationWithStartupGate.web.js +9 -0
  54. package/lib/module/platform/DataverseDataSources.js +10 -0
  55. package/lib/module/platform/DataverseGlobalDiscoveryApi.js +45 -0
  56. package/lib/module/platform/GlobalDiscovery.js +45 -0
  57. package/lib/module/platform/PowerPlatformEnvironmentApi.js +5 -104
  58. package/lib/module/provider/PowerAppsHostProvider.js +159 -28
  59. package/lib/module/provider/PowerAppsProvider.js +177 -138
  60. package/lib/module/provider/SessionDetailsGesture.js +211 -0
  61. package/lib/module/telemetry/OfflineSdkTelemetry.js +91 -0
  62. package/lib/module/telemetry/OfflineSdkTelemetry.web.js +5 -0
  63. package/lib/module/telemetry/index.js +15 -4
  64. package/lib/typescript/commonjs/auth/AuthContext.d.ts +3 -1
  65. package/lib/typescript/commonjs/auth/AuthContext.web.d.ts +1 -0
  66. package/lib/typescript/commonjs/bridge/bridgeState.d.ts +5 -0
  67. package/lib/typescript/commonjs/connections/ConnectionSetupScreen.d.ts +28 -7
  68. package/lib/typescript/commonjs/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  69. package/lib/typescript/commonjs/connections/connectionMetadata.d.ts +66 -0
  70. package/lib/typescript/commonjs/connections/connectionState.d.ts +16 -0
  71. package/lib/typescript/commonjs/connections/disambiguate.d.ts +4 -2
  72. package/lib/typescript/commonjs/{platform → connections}/resolveConnections.d.ts +19 -19
  73. package/lib/typescript/commonjs/connections/types.d.ts +129 -1
  74. package/lib/typescript/commonjs/{platform → connections}/useConnectionRefs.d.ts +33 -24
  75. package/lib/typescript/commonjs/connections/useConnectionSetup.d.ts +12 -8
  76. package/lib/typescript/commonjs/controls/FilePicker.d.ts +7 -1
  77. package/lib/typescript/commonjs/controls/ImagePicker.d.ts +9 -1
  78. package/lib/typescript/commonjs/index.d.ts +17 -10
  79. package/lib/typescript/commonjs/logger.d.ts +4 -4
  80. package/lib/typescript/commonjs/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  81. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  82. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  83. package/lib/typescript/commonjs/platform/DataverseDataSources.d.ts +27 -0
  84. package/lib/typescript/commonjs/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  85. package/lib/typescript/commonjs/platform/GlobalDiscovery.d.ts +4 -0
  86. package/lib/typescript/commonjs/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  87. package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +29 -18
  88. package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +30 -8
  89. package/lib/typescript/commonjs/provider/SessionDetailsGesture.d.ts +13 -0
  90. package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.d.ts +15 -0
  91. package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
  92. package/lib/typescript/commonjs/telemetry/index.d.ts +13 -1
  93. package/lib/typescript/module/auth/AuthContext.d.ts +3 -1
  94. package/lib/typescript/module/auth/AuthContext.web.d.ts +1 -0
  95. package/lib/typescript/module/bridge/bridgeState.d.ts +5 -0
  96. package/lib/typescript/module/connections/ConnectionSetupScreen.d.ts +28 -7
  97. package/lib/typescript/module/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  98. package/lib/typescript/module/connections/connectionMetadata.d.ts +66 -0
  99. package/lib/typescript/module/connections/connectionState.d.ts +16 -0
  100. package/lib/typescript/module/connections/disambiguate.d.ts +4 -2
  101. package/lib/typescript/module/{platform → connections}/resolveConnections.d.ts +19 -19
  102. package/lib/typescript/module/connections/types.d.ts +129 -1
  103. package/lib/typescript/module/{platform → connections}/useConnectionRefs.d.ts +33 -24
  104. package/lib/typescript/module/connections/useConnectionSetup.d.ts +12 -8
  105. package/lib/typescript/module/controls/FilePicker.d.ts +7 -1
  106. package/lib/typescript/module/controls/ImagePicker.d.ts +9 -1
  107. package/lib/typescript/module/index.d.ts +17 -10
  108. package/lib/typescript/module/logger.d.ts +4 -4
  109. package/lib/typescript/module/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  110. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  111. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  112. package/lib/typescript/module/platform/DataverseDataSources.d.ts +27 -0
  113. package/lib/typescript/module/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  114. package/lib/typescript/module/platform/GlobalDiscovery.d.ts +4 -0
  115. package/lib/typescript/module/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  116. package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +29 -18
  117. package/lib/typescript/module/provider/PowerAppsProvider.d.ts +30 -8
  118. package/lib/typescript/module/provider/SessionDetailsGesture.d.ts +13 -0
  119. package/lib/typescript/module/telemetry/OfflineSdkTelemetry.d.ts +15 -0
  120. package/lib/typescript/module/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
  121. package/lib/typescript/module/telemetry/index.d.ts +13 -1
  122. package/package.json +11 -6
  123. package/scripts/metro-logger.js +213 -0
  124. package/scripts/upgrade/runtime.js +13 -7
  125. package/scripts/upgrade-authoring.js +5 -6
  126. package/lib/commonjs/platform/PowerPlatformConnectionApi.js +0 -606
  127. package/lib/commonjs/platform/resolveConnections.js +0 -264
  128. package/lib/module/platform/PowerPlatformConnectionApi.js +0 -595
  129. package/lib/module/platform/resolveConnections.js +0 -259
@@ -3,14 +3,7 @@
3
3
  /*!
4
4
  * power-apps-native-host — PowerPlatformEnvironmentApi
5
5
  *
6
- * Mirrors the Host SDK's DefaultAppMetadataProvider.fetchAppMetadataFromPowerConfig
7
- * pattern: resolves the Power Platform environment-scoped API endpoint from the
8
- * environment GUID and region, then fetches linkedEnvironmentMetadata to obtain
9
- * the Dataverse instanceUrl.
10
- *
11
- * Reference (Host SDK):
12
- * packages/power-apps-host/src/providers/defaults/DefaultAppMetadataProvider.ts
13
- * packages/power-apps-common/src/services/PowerPlatformApiDiscovery.ts
6
+ * Resolves Power Platform RP endpoints and scopes.
14
7
  */
15
8
 
16
9
  // ---------------------------------------------------------------------------
@@ -69,10 +62,6 @@ export function buildEnvironmentEndpoint(environmentId, region) {
69
62
  return `https://${hexPrefix}.${hexSuffix}.environment.${hostNameSuffix}`;
70
63
  }
71
64
 
72
- // ---------------------------------------------------------------------------
73
- // Public API
74
- // ---------------------------------------------------------------------------
75
-
76
65
  /**
77
66
  * Token scope for the Power Platform management API (prod cluster).
78
67
  * Non-prod clusters (test, gov, high, dev, mooncake, etc.) host the management
@@ -90,97 +79,9 @@ export function powerPlatformScope(region) {
90
79
  const host = HOST_NAME_SUFFIX[region] ?? HOST_NAME_SUFFIX['prod'];
91
80
  return `https://${host}/.default`;
92
81
  }
93
- import { getAppLogger, HostOperationNames } from "../telemetry/index.js";
94
- function getSafeErrorName(error) {
95
- return error instanceof Error ? error.constructor.name : typeof error;
96
- }
97
82
 
98
- /**
99
- * Fetch the Dataverse instanceUrl for a Power Platform environment by calling
100
- * the environment-scoped Power Platform management API — the same call that
101
- * the Host SDK's DefaultAppMetadataProvider makes to populate
102
- * `databaseReferences[*].databaseDetails.linkedEnvironmentMetadata`.
103
- *
104
- * @param environmentId - Environment GUID (from power.config.json `environmentId`).
105
- * @param region - Cluster region string (from power.config.json `region`, default "prod").
106
- * @param acquireToken - Callback to obtain a bearer token for the given scopes.
107
- * Scope will be {@link powerPlatformScope}`(region)`.
108
- * @param bearerToken - Optional pre-fetched bearer token for {@link powerPlatformScope}`(region)`.
109
- * Pass this to avoid a redundant `acquireToken` round-trip when the
110
- * caller has already obtained a token for the same audience.
111
- * @returns The Dataverse org URL (e.g. "https://org12345.crm.dynamics.com"), or
112
- * `undefined` if the environment has no linked CDS database.
113
- */
114
- export async function fetchInstanceUrl(environmentId, region, acquireToken, bearerToken) {
115
- const scenario = getAppLogger().trackScenario(HostOperationNames.PlatformFetchInstanceUrl, {
116
- environmentId,
117
- region
118
- });
119
- try {
120
- const baseUrl = buildEnvironmentEndpoint(environmentId, region);
121
- const url = `${baseUrl}/powerapps/environment?api-version=1&%24filter=name+eq+%27${encodeURIComponent(environmentId)}%27`;
122
- const token = bearerToken ?? (await acquireToken([powerPlatformScope(region)]));
123
- if (!token) {
124
- throw new Error('fetchInstanceUrl: failed to acquire Power Platform API token');
125
- }
126
- const response = await fetch(url, {
127
- method: 'GET',
128
- headers: {
129
- Authorization: `Bearer ${token}`,
130
- 'Content-Type': 'application/json'
131
- }
132
- });
133
- if (!response.ok) {
134
- throw new Error(`fetchInstanceUrl: environment API returned ${response.status} ${response.statusText}`);
135
- }
136
- const env = await response.json();
137
- const instanceUrl = env.properties?.linkedEnvironmentMetadata?.instanceUrl;
138
- scenario.complete({
139
- hasInstanceUrl: instanceUrl !== undefined
140
- });
141
- return instanceUrl;
142
- } catch (e) {
143
- scenario.fail(undefined, {
144
- errorName: getSafeErrorName(e)
145
- });
146
- throw e;
147
- }
148
- }
149
-
150
- // ---------------------------------------------------------------------------
151
- // buildCdsDataSources
152
- // ---------------------------------------------------------------------------
153
-
154
- /**
155
- * Shape of a single Dataverse table entry inside power.config.json
156
- * `databaseReferences[*].dataSources[*]`.
157
- */
158
-
159
- /**
160
- * Shape of a single database reference entry inside power.config.json
161
- * `databaseReferences`.
162
- */
163
-
164
- /**
165
- * Build the CDS data source config array that the SDK reads via
166
- * AppPowerAppsClientPlugin.getAppCdsDataSourceConfigsAsync.
167
- *
168
- * Pass the resolved Dataverse org URL (from useConnectionRefs) and the
169
- * databaseReferences map from power.config.json. The resulting array is
170
- * passed directly to <PowerAppsHostProvider dataSources={...} />.
171
- *
172
- * @param databaseReferences - The `databaseReferences` map from power.config.json.
173
- * @param dataverseOrgUrl - The resolved Dataverse org URL
174
- * (connectionRefs['commondataserviceforapps']?.datasetName).
175
- * Pass an empty string before the org URL is known;
176
- * the host provider will receive empty runtimeUrl entries
177
- * and the SDK will not attempt Dataverse calls.
178
- */
179
- export function buildCdsDataSources(databaseReferences, dataverseOrgUrl) {
180
- return Object.values(databaseReferences).flatMap(dbRef => Object.values(dbRef.dataSources ?? {}).map(ds => ({
181
- runtimeUrl: dataverseOrgUrl ? `${dataverseOrgUrl}/api/data/v9.0/` : '',
182
- entitySetName: ds.entitySetName ?? '',
183
- logicalName: ds.logicalName ?? '',
184
- version: '9.2'
185
- })));
83
+ /** Delegated permission required by the connection user-consent endpoint. */
84
+ export function connectionUserConsentScope(region) {
85
+ const host = HOST_NAME_SUFFIX[region] ?? HOST_NAME_SUFFIX['prod'];
86
+ return `https://${host}/Connectivity.Connections.UserConsent`;
186
87
  }
@@ -30,20 +30,26 @@
30
30
  */
31
31
 
32
32
  import React, { useEffect, useLayoutEffect, useRef, useState, useCallback, useMemo } from 'react';
33
- import { ActivityIndicator, View } from 'react-native';
33
+ import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';
34
34
  import { SafeAreaProvider } from 'react-native-safe-area-context';
35
35
  import { setupNativeHost } from "../bridge/setupNativeHost.js";
36
36
  import { buildConnectionReferences } from "../bridge/NativePowerAppsBridge.js";
37
- import { useConnectionRefs } from "../platform/useConnectionRefs.js";
38
- import { buildCdsDataSources } from "../platform/PowerPlatformEnvironmentApi.js";
37
+ import { useConnectionRefs } from "../connections/useConnectionRefs.js";
38
+ import { buildCdsDataSources } from "../platform/DataverseDataSources.js";
39
+ import { powerPlatformScope } from "../platform/PowerPlatformEnvironmentApi.js";
39
40
  import { ConnectionSetupScreen } from "../connections/ConnectionSetupScreen.js";
41
+ import { buildConnectionResolutionKey } from "../connections/types.js";
42
+ import { postConnectionUsage, reportScopeConsents } from "../connections/PowerPlatformConnectionApi.js";
43
+ import { shouldPersistConnectionUsages, shouldShowConnectionReview } from "../connections/connectionState.js";
44
+ import { isDevPlayer } from "../utils/isDevPlayer.js";
40
45
  import { debugLog } from "../logger.js";
41
46
  import { getAppLogger, HostOperationNames } from "../telemetry/index.js";
42
47
 
43
48
  // Entra ID token scope for the APIM connector proxy (API Hub).
44
- import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
49
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
45
50
  const CONNECTOR_SCOPE = 'https://apihub.azure.com/.default';
46
51
  const EMPTY_DATABASE_REFERENCES = {};
52
+ const EMPTY_CONNECTION_REFERENCES = {};
47
53
  /**
48
54
  * Initialises the NativePowerAppsBridge and manages the full pre-launch
49
55
  * pipeline — connection resolution, disambiguation, and creation — before
@@ -54,6 +60,7 @@ const EMPTY_DATABASE_REFERENCES = {};
54
60
  export function PowerAppsHostProvider({
55
61
  acquireToken,
56
62
  isSignedIn,
63
+ onSignOut,
57
64
  appId,
58
65
  environmentId,
59
66
  region = 'prod',
@@ -63,18 +70,75 @@ export function PowerAppsHostProvider({
63
70
  renderOAuthWebView,
64
71
  isvClientId,
65
72
  onDisambiguationNeeded,
73
+ onConnectionConsent,
74
+ onResourceConsent,
75
+ onConnectionConsentDeclined,
66
76
  children
67
77
  }) {
68
78
  // Extra refs produced by ConnectionSetupScreen for newly-created connections.
69
79
  const [extraConnectionRefs, setExtraConnectionRefs] = useState({});
70
-
71
- // setupDone is derived — true as soon as extraConnectionRefs is populated.
72
- const setupDone = Object.keys(extraConnectionRefs).length > 0;
80
+ const [consentApproved, setConsentApproved] = useState(false);
81
+ const setupDone = consentApproved;
82
+ const renderConnectionError = (title, messages, useNeutralText = false) => /*#__PURE__*/_jsxs(View, {
83
+ style: {
84
+ flex: 1,
85
+ padding: 24,
86
+ justifyContent: 'center',
87
+ backgroundColor: '#F8F8F8'
88
+ },
89
+ children: [/*#__PURE__*/_jsx(Text, {
90
+ style: {
91
+ fontSize: 20,
92
+ fontWeight: '700',
93
+ color: '#1A1A1A',
94
+ marginBottom: 12
95
+ },
96
+ children: title
97
+ }), messages.map(message => /*#__PURE__*/_jsx(Text, {
98
+ style: {
99
+ fontSize: 14,
100
+ color: useNeutralText ? '#323130' : '#C4262E',
101
+ lineHeight: 20,
102
+ marginBottom: 8
103
+ },
104
+ children: message
105
+ }, message)), isSignedIn && onSignOut ? /*#__PURE__*/_jsx(TouchableOpacity, {
106
+ accessibilityRole: "button",
107
+ onPress: () => {
108
+ void onSignOut();
109
+ },
110
+ style: {
111
+ alignSelf: 'flex-start',
112
+ backgroundColor: '#0F6CBD',
113
+ paddingHorizontal: 20,
114
+ paddingVertical: 12,
115
+ borderRadius: 4,
116
+ marginTop: 12
117
+ },
118
+ children: /*#__PURE__*/_jsx(Text, {
119
+ style: {
120
+ color: '#FFFFFF',
121
+ fontSize: 15,
122
+ fontWeight: '600'
123
+ },
124
+ children: "Sign out"
125
+ })
126
+ }) : null]
127
+ });
73
128
  const {
74
129
  connectionRefs,
75
130
  isReady,
131
+ error: connectionError,
132
+ errorKind: connectionErrorKind,
76
133
  missingConnectors,
77
- dataverseOrgUrl
134
+ missingConnectionReferences,
135
+ connectionRequirements,
136
+ resourceConsents: launchResourceConsents,
137
+ configurationErrors,
138
+ requiresConnectionConsent,
139
+ resolvedConfigKey,
140
+ dataverseOrgUrl,
141
+ databaseReferences: resolvedDatabaseReferences
78
142
  } = useConnectionRefs({
79
143
  isSignedIn,
80
144
  acquireToken,
@@ -92,39 +156,43 @@ export function PowerAppsHostProvider({
92
156
  if (!isSignedIn) {
93
157
  debugLog('[PAHost] user signed out — resetting bridge state');
94
158
  setExtraConnectionRefs({});
159
+ setConsentApproved(false);
95
160
  }
96
161
  }, [isSignedIn]);
162
+ useEffect(() => {
163
+ setExtraConnectionRefs({});
164
+ setConsentApproved(false);
165
+ }, [appId, environmentId, connectionReferences]);
97
166
 
98
167
  // Merge hook refs with anything the setup screen created.
99
168
  const mergedRefs = useMemo(() => ({
100
169
  ...connectionRefs,
101
170
  ...extraConnectionRefs
102
171
  }), [connectionRefs, extraConnectionRefs]);
103
- const staticConnectionRefs = useMemo(() => buildConnectionReferences(connectionReferences), [connectionReferences]);
172
+ const staticConnectionRefs = useMemo(() => isDevPlayer ? buildConnectionReferences(connectionReferences) : EMPTY_CONNECTION_REFERENCES, [connectionReferences, isDevPlayer]);
104
173
 
105
174
  // dataverseOrgUrl comes directly from useConnectionRefs (fetched from the
106
175
  // environment metadata regardless of whether a commondataserviceforapps
107
176
  // connection reference is configured). This allows Dataverse calls to work
108
177
  // even when there is no explicit Dataverse connection in connectionReferences.
109
- const cdsDataSources = useMemo(() => buildCdsDataSources(databaseReferences ?? EMPTY_DATABASE_REFERENCES, dataverseOrgUrl), [databaseReferences, dataverseOrgUrl]);
178
+ const cdsDataSources = useMemo(() => buildCdsDataSources(isDevPlayer ? databaseReferences ?? EMPTY_DATABASE_REFERENCES : resolvedDatabaseReferences, dataverseOrgUrl), [databaseReferences, resolvedDatabaseReferences, dataverseOrgUrl, isDevPlayer]);
110
179
 
111
180
  // Build the missing-connector entries for ConnectionSetupScreen.
112
- const missingEntries = useMemo(() => missingConnectors.map(apiId => {
113
- const localRefId = Object.keys(connectionReferences).find(k => connectionReferences[k].id === apiId) ?? apiId;
114
- return {
115
- apiId,
116
- localRefId
117
- };
118
- }), [missingConnectors, connectionReferences]);
181
+ const missingEntries = useMemo(() => missingConnectionReferences, [missingConnectionReferences]);
182
+ const resourceConsents = useMemo(() => [...new Map(launchResourceConsents.map(resource => [resource.resourceId.toLowerCase(), resource])).values()], [launchResourceConsents]);
183
+ const currentConfigKey = useMemo(() => buildConnectionResolutionKey(appId, environmentId, isDevPlayer ? connectionReferences : {}), [appId, environmentId, connectionReferences, isDevPlayer]);
184
+ const resolutionMatchesCurrentConfig = resolvedConfigKey === currentConfigKey;
185
+ const needsConsent = shouldShowConnectionReview(isDevPlayer, requiresConnectionConsent, resourceConsents.length > 0);
119
186
 
120
187
  // Called by ConnectionSetupScreen once every missing connector is resolved.
121
- const handleSetupComplete = useCallback(resolvedIds => {
188
+ const handleSetupComplete = useCallback(async resolvedIds => {
122
189
  const refs = {};
123
- for (const [apiId, connectionId] of Object.entries(resolvedIds)) {
124
- const localRefId = Object.keys(connectionReferences).find(k => connectionReferences[k].id === apiId);
125
- if (!localRefId) continue;
126
- const dataSources = connectionReferences[localRefId].dataSources;
127
- const runtimeUrl = dataSources.map(ds => schemaMap[ds]?.properties?.primaryRuntimeUrl).find(Boolean) ?? '';
190
+ for (const [localRefId, connectionId] of Object.entries(resolvedIds)) {
191
+ const requirement = connectionRequirements.find(item => item.localRefId === localRefId);
192
+ if (!requirement) continue;
193
+ const apiId = requirement.apiId;
194
+ const dataSources = requirement.dataSources;
195
+ const runtimeUrl = dataSources.map(ds => connectionRefsRef.current[ds]?.runtimeUrl ?? (isDevPlayer ? schemaMap[ds]?.properties?.primaryRuntimeUrl : undefined)).find(Boolean) ?? '';
128
196
  for (const ds of dataSources) {
129
197
  refs[ds] = {
130
198
  apiId,
@@ -138,7 +206,44 @@ export function PowerAppsHostProvider({
138
206
  }
139
207
  }
140
208
  setExtraConnectionRefs(refs);
141
- }, [connectionReferences, schemaMap]);
209
+ const mergedForConsent = {
210
+ ...connectionRefsRef.current,
211
+ ...refs
212
+ };
213
+ const usages = connectionRequirements.map(requirement => ({
214
+ localRefId: requirement.localRefId,
215
+ apiId: requirement.apiId,
216
+ connectionId: requirement.dataSources.map(dataSource => mergedForConsent[dataSource]?.connectionName).find(Boolean) ?? '',
217
+ dataSources: requirement.dataSources,
218
+ appActions: requirement.appActions ?? [],
219
+ apiScopes: requirement.apiScopes
220
+ }));
221
+ if (usages.some(usage => !usage.connectionId)) {
222
+ throw new Error('Not all required connections have been resolved.');
223
+ }
224
+ if (!isDevPlayer && resourceConsents.length > 0 && !onResourceConsent) {
225
+ throw new Error('Protected resource consent requires an onResourceConsent persistence callback.');
226
+ }
227
+ const shouldPersistUsages = shouldPersistConnectionUsages(isDevPlayer, appId);
228
+ if (shouldPersistUsages && appId && usages.length > 0) {
229
+ const token = await acquireToken([powerPlatformScope(region)]);
230
+ if (!token) throw new Error('Unable to acquire a token to register connection usage.');
231
+ await Promise.all(usages.map(usage => postConnectionUsage(appId, usage, environmentId, region, token)));
232
+ await reportScopeConsents(appId, usages, environmentId, region, token);
233
+ }
234
+ if (!isDevPlayer) {
235
+ await onConnectionConsent?.(usages);
236
+ await onResourceConsent?.(resourceConsents);
237
+ }
238
+ setConsentApproved(true);
239
+ }, [acquireToken, appId, environmentId, region, schemaMap, connectionRequirements, onConnectionConsent, onResourceConsent, resourceConsents, isDevPlayer]);
240
+ const consentDetails = useMemo(() => connectionRequirements.map(requirement => ({
241
+ localRefId: requirement.localRefId,
242
+ connectorId: requirement.connectorId,
243
+ displayName: requirement.displayName ?? requirement.connectorId.replace(/^shared_/, ''),
244
+ description: `Uses ${requirement.dataSources.join(', ')}`,
245
+ appActions: requirement.appActions ?? []
246
+ })), [connectionRequirements]);
142
247
 
143
248
  // ---- Bridge wiring ----
144
249
 
@@ -150,7 +255,7 @@ export function PowerAppsHostProvider({
150
255
  connectionRefsRef.current = connectionRefs;
151
256
  const configuredBridgeInputsRef = useRef(undefined);
152
257
  const [, setBridgeRegistrationVersion] = useState(0);
153
- const hasResolvedConnections = isSignedIn && isReady && (missingConnectors.length === 0 || setupDone);
258
+ const hasResolvedConnections = isSignedIn && isReady && resolutionMatchesCurrentConfig && (missingConnectors.length === 0 || consentApproved) && (!needsConsent || consentApproved);
154
259
  const isBridgeConfiguredFor = (connections, dataSources) => {
155
260
  const currentInputs = configuredBridgeInputsRef.current;
156
261
  return currentInputs?.connections === connections && currentInputs.dataSources === dataSources;
@@ -250,13 +355,34 @@ export function PowerAppsHostProvider({
250
355
  }
251
356
  }, [isSignedIn, isReady]);
252
357
  useEffect(() => {
253
- if (isSignedIn && isReady && missingConnectors.length > 0 && !setupDone) {
358
+ if (isSignedIn && isReady && (missingConnectors.length > 0 || needsConsent) && !setupDone) {
254
359
  debugLog(`[PAHost] render: showing ConnectionSetupScreen for ${missingConnectors.length} missing connector(s):`, missingConnectors.join(', '));
255
360
  }
256
- }, [isSignedIn, isReady, missingConnectors, setupDone]);
361
+ }, [isSignedIn, isReady, missingConnectors, needsConsent, setupDone]);
257
362
 
258
363
  // ---- Render ----
259
364
 
365
+ if (connectionError) {
366
+ const accessDenied = connectionErrorKind === 'app-access-denied';
367
+ return renderConnectionError(accessDenied ? 'You don’t have access' : 'Unable to load app', [connectionError], accessDenied);
368
+ }
369
+ if (configurationErrors.length > 0) {
370
+ return renderConnectionError('Connection configuration error', configurationErrors);
371
+ }
372
+ if (isSignedIn && !resolutionMatchesCurrentConfig) {
373
+ return /*#__PURE__*/_jsx(View, {
374
+ style: {
375
+ flex: 1,
376
+ alignItems: 'center',
377
+ justifyContent: 'center',
378
+ backgroundColor: '#F8F8F8'
379
+ },
380
+ children: /*#__PURE__*/_jsx(ActivityIndicator, {
381
+ size: "large"
382
+ })
383
+ });
384
+ }
385
+
260
386
  // First mount: register the native bridge with static manifest refs before
261
387
  // any route children can mount and call into @microsoft/power-apps.
262
388
  if (!staticBridgeRegistered && !hasResolvedConnections) {
@@ -292,7 +418,7 @@ export function PowerAppsHostProvider({
292
418
  // Wrapped in SafeAreaProvider so useSafeAreaInsets() works even when the
293
419
  // consuming app hasn't mounted its own SafeAreaProvider yet. Nesting is
294
420
  // safe — react-native-safe-area-context treats inner providers as no-ops.
295
- if (isSignedIn && isReady && missingConnectors.length > 0 && !setupDone) {
421
+ if (isSignedIn && isReady && (missingConnectors.length > 0 || needsConsent) && !setupDone) {
296
422
  return /*#__PURE__*/_jsx(SafeAreaProvider, {
297
423
  children: /*#__PURE__*/_jsx(ConnectionSetupScreen, {
298
424
  missingConnectors: missingEntries,
@@ -300,8 +426,13 @@ export function PowerAppsHostProvider({
300
426
  environmentId: environmentId,
301
427
  region: region,
302
428
  acquireToken: acquireToken,
429
+ onSignOut: isSignedIn ? onSignOut : undefined,
303
430
  isvClientId: isvClientId,
304
431
  onAllConnected: handleSetupComplete,
432
+ consentDetails: consentDetails,
433
+ resourceConsents: resourceConsents,
434
+ onConsentDeclined: onConnectionConsentDeclined,
435
+ showReview: !isDevPlayer,
305
436
  renderOAuthWebView: renderOAuthWebView
306
437
  // No onSkip — connections are required.
307
438
  })