@microsoft/power-apps-native-host 0.2.22

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 (104) hide show
  1. package/LICENSE +112 -0
  2. package/README.md +59 -0
  3. package/app.plugin.js +4 -0
  4. package/lib/NOTICE.txt +1659 -0
  5. package/lib/commonjs/ErrorBoundary.js +128 -0
  6. package/lib/commonjs/auth/AuthContext.js +281 -0
  7. package/lib/commonjs/bridge/NativePowerAppsBridge.js +279 -0
  8. package/lib/commonjs/bridge/setupNativeHost.js +65 -0
  9. package/lib/commonjs/connections/ConnectionSetupScreen.js +531 -0
  10. package/lib/commonjs/connections/disambiguate.js +88 -0
  11. package/lib/commonjs/connections/types.js +58 -0
  12. package/lib/commonjs/connections/useConnectionSetup.js +236 -0
  13. package/lib/commonjs/controls/FilePicker.js +183 -0
  14. package/lib/commonjs/controls/ImagePicker.js +236 -0
  15. package/lib/commonjs/index.js +246 -0
  16. package/lib/commonjs/logger.js +27 -0
  17. package/lib/commonjs/package.json +1 -0
  18. package/lib/commonjs/platform/PowerPlatformConnectionApi.js +606 -0
  19. package/lib/commonjs/platform/PowerPlatformEnvironmentApi.js +194 -0
  20. package/lib/commonjs/platform/resolveConnections.js +264 -0
  21. package/lib/commonjs/platform/useConnectionRefs.js +270 -0
  22. package/lib/commonjs/provider/PowerAppsHostProvider.js +336 -0
  23. package/lib/commonjs/provider/PowerAppsProvider.js +351 -0
  24. package/lib/commonjs/telemetry/index.js +70 -0
  25. package/lib/commonjs/theme/ThemeProvider.js +80 -0
  26. package/lib/commonjs/theme/tokens.js +50 -0
  27. package/lib/commonjs/utils/dataverseUtils.js +98 -0
  28. package/lib/commonjs/utils/isDevPlayer.js +27 -0
  29. package/lib/module/ErrorBoundary.js +122 -0
  30. package/lib/module/auth/AuthContext.js +276 -0
  31. package/lib/module/bridge/NativePowerAppsBridge.js +273 -0
  32. package/lib/module/bridge/setupNativeHost.js +62 -0
  33. package/lib/module/connections/ConnectionSetupScreen.js +526 -0
  34. package/lib/module/connections/disambiguate.js +83 -0
  35. package/lib/module/connections/types.js +60 -0
  36. package/lib/module/connections/useConnectionSetup.js +233 -0
  37. package/lib/module/controls/FilePicker.js +178 -0
  38. package/lib/module/controls/ImagePicker.js +231 -0
  39. package/lib/module/index.js +41 -0
  40. package/lib/module/logger.js +22 -0
  41. package/lib/module/package.json +1 -0
  42. package/lib/module/platform/PowerPlatformConnectionApi.js +595 -0
  43. package/lib/module/platform/PowerPlatformEnvironmentApi.js +186 -0
  44. package/lib/module/platform/resolveConnections.js +259 -0
  45. package/lib/module/platform/useConnectionRefs.js +267 -0
  46. package/lib/module/provider/PowerAppsHostProvider.js +332 -0
  47. package/lib/module/provider/PowerAppsProvider.js +345 -0
  48. package/lib/module/telemetry/index.js +55 -0
  49. package/lib/module/theme/ThemeProvider.js +63 -0
  50. package/lib/module/theme/tokens.js +46 -0
  51. package/lib/module/utils/dataverseUtils.js +90 -0
  52. package/lib/module/utils/isDevPlayer.js +23 -0
  53. package/lib/typescript/commonjs/ErrorBoundary.d.ts +30 -0
  54. package/lib/typescript/commonjs/auth/AuthContext.d.ts +57 -0
  55. package/lib/typescript/commonjs/bridge/NativePowerAppsBridge.d.ts +112 -0
  56. package/lib/typescript/commonjs/bridge/setupNativeHost.d.ts +39 -0
  57. package/lib/typescript/commonjs/connections/ConnectionSetupScreen.d.ts +107 -0
  58. package/lib/typescript/commonjs/connections/disambiguate.d.ts +43 -0
  59. package/lib/typescript/commonjs/connections/types.d.ts +91 -0
  60. package/lib/typescript/commonjs/connections/useConnectionSetup.d.ts +96 -0
  61. package/lib/typescript/commonjs/controls/FilePicker.d.ts +65 -0
  62. package/lib/typescript/commonjs/controls/ImagePicker.d.ts +76 -0
  63. package/lib/typescript/commonjs/index.d.ts +42 -0
  64. package/lib/typescript/commonjs/logger.d.ts +11 -0
  65. package/lib/typescript/commonjs/package.json +1 -0
  66. package/lib/typescript/commonjs/platform/PowerPlatformConnectionApi.d.ts +205 -0
  67. package/lib/typescript/commonjs/platform/PowerPlatformEnvironmentApi.d.ts +108 -0
  68. package/lib/typescript/commonjs/platform/resolveConnections.d.ts +86 -0
  69. package/lib/typescript/commonjs/platform/useConnectionRefs.d.ts +126 -0
  70. package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +123 -0
  71. package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +148 -0
  72. package/lib/typescript/commonjs/telemetry/index.d.ts +48 -0
  73. package/lib/typescript/commonjs/theme/ThemeProvider.d.ts +30 -0
  74. package/lib/typescript/commonjs/theme/tokens.d.ts +27 -0
  75. package/lib/typescript/commonjs/utils/dataverseUtils.d.ts +39 -0
  76. package/lib/typescript/commonjs/utils/isDevPlayer.d.ts +19 -0
  77. package/lib/typescript/module/ErrorBoundary.d.ts +30 -0
  78. package/lib/typescript/module/auth/AuthContext.d.ts +57 -0
  79. package/lib/typescript/module/bridge/NativePowerAppsBridge.d.ts +112 -0
  80. package/lib/typescript/module/bridge/setupNativeHost.d.ts +39 -0
  81. package/lib/typescript/module/connections/ConnectionSetupScreen.d.ts +107 -0
  82. package/lib/typescript/module/connections/disambiguate.d.ts +43 -0
  83. package/lib/typescript/module/connections/types.d.ts +91 -0
  84. package/lib/typescript/module/connections/useConnectionSetup.d.ts +96 -0
  85. package/lib/typescript/module/controls/FilePicker.d.ts +65 -0
  86. package/lib/typescript/module/controls/ImagePicker.d.ts +76 -0
  87. package/lib/typescript/module/index.d.ts +42 -0
  88. package/lib/typescript/module/logger.d.ts +11 -0
  89. package/lib/typescript/module/package.json +1 -0
  90. package/lib/typescript/module/platform/PowerPlatformConnectionApi.d.ts +205 -0
  91. package/lib/typescript/module/platform/PowerPlatformEnvironmentApi.d.ts +108 -0
  92. package/lib/typescript/module/platform/resolveConnections.d.ts +86 -0
  93. package/lib/typescript/module/platform/useConnectionRefs.d.ts +126 -0
  94. package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +123 -0
  95. package/lib/typescript/module/provider/PowerAppsProvider.d.ts +148 -0
  96. package/lib/typescript/module/telemetry/index.d.ts +48 -0
  97. package/lib/typescript/module/theme/ThemeProvider.d.ts +30 -0
  98. package/lib/typescript/module/theme/tokens.d.ts +27 -0
  99. package/lib/typescript/module/utils/dataverseUtils.d.ts +39 -0
  100. package/lib/typescript/module/utils/isDevPlayer.d.ts +19 -0
  101. package/package.json +119 -0
  102. package/scripts/build-ios-base.js +96 -0
  103. package/scripts/bundle.js +95 -0
  104. package/scripts/generate-connector-schemas.js +75 -0
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+
3
+ /*!
4
+ * power-apps-native-host — setupNativeHost
5
+ *
6
+ * Registers a NativePowerAppsBridge on the JS global (globalThis.powerAppsBridge)
7
+ * so the @microsoft/power-apps SDK picks it up automatically on the first data
8
+ * operation. The SDK reads globalThis.powerAppsBridge in getBridge() before
9
+ * falling back to the web DefaultPowerAppsBridge.
10
+ *
11
+ * Call this once at app root before any data operations. Re-calling with updated
12
+ * options (e.g. when connections change) is safe — it updates the existing bridge
13
+ * in-place without resetting the SDK's cached bridge promise.
14
+ */
15
+
16
+ import { NativePowerAppsBridge } from "./NativePowerAppsBridge.js";
17
+ import { debugLog } from "../logger.js";
18
+
19
+ /** The single bridge instance for the lifetime of the app. */
20
+ let _bridge;
21
+
22
+ /**
23
+ * Registers a NativePowerAppsBridge for all @microsoft/power-apps SDK calls.
24
+ *
25
+ * On first call, creates a NativePowerAppsBridge and injects it into
26
+ * `globalThis.powerAppsBridge`. Subsequent calls update the bridge's options
27
+ * in-place so connection references and data sources can change without
28
+ * rebuilding the bridge (important because the SDK caches the bridge instance
29
+ * after first use).
30
+ *
31
+ * Idempotent with the same options reference — no-op if nothing changed.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * // App.tsx (root)
36
+ * import { setupNativeHost } from '@microsoft/power-apps-native-host';
37
+ *
38
+ * setupNativeHost({
39
+ * getConnectorToken: (apiId) => msalClient.acquireToken({ resource: apiId }),
40
+ * getDataverseToken: (datasetName) => msalClient.acquireToken({ resource: datasetName }),
41
+ * dataSources: appManifest.dataSources,
42
+ * connections: appManifest.connections,
43
+ * });
44
+ * ```
45
+ */
46
+ export function setupNativeHost(options) {
47
+ if (_bridge) {
48
+ // Bridge already created — update options in-place. The SDK caches the
49
+ // bridge instance via bridgePromise, so replacing globalThis.powerAppsBridge
50
+ // would have no effect after the first data operation.
51
+ debugLog('[bridge] setupNativeHost: updating existing bridge options');
52
+ _bridge.updateOptions(options);
53
+ } else {
54
+ debugLog('[bridge] setupNativeHost: creating NativePowerAppsBridge and injecting into globalThis');
55
+ _bridge = new NativePowerAppsBridge(options);
56
+ // Inject into the JS global so the SDK finds it in getBridge().
57
+ // In browsers, globalThis === window, so window.powerAppsBridge also resolves.
58
+ // In React Native, globalThis is the Hermes global object.
59
+ globalThis.powerAppsBridge = _bridge;
60
+ debugLog('[bridge] setupNativeHost: bridge ready');
61
+ }
62
+ }
@@ -0,0 +1,526 @@
1
+ "use strict";
2
+
3
+ /*!
4
+ * power-apps-native-host — ConnectionSetupScreen
5
+ *
6
+ * A ready-made React Native screen that drives the "create missing connections"
7
+ * flow before the Power Apps player is shown.
8
+ *
9
+ * Intended usage in an expo-router layout:
10
+ *
11
+ * // app/(app)/_layout.tsx
12
+ * import { ConnectionSetupScreen } from '@microsoft/power-apps-native-host';
13
+ *
14
+ * if (missingConnectors.length > 0) {
15
+ * return (
16
+ * <ConnectionSetupScreen
17
+ * missingConnectors={missingConnectors} // from useConnectionRefs
18
+ * schemaMap={schemaMap}
19
+ * environmentId={powerConfig.environmentId}
20
+ * region={powerConfig.region}
21
+ * acquireToken={acquireToken}
22
+ * onAllConnected={(resolvedIds) => {
23
+ * // resolvedIds: Record<apiId, connectionId>
24
+ * // Store them, then set missingConnectors = [] to unmount this screen.
25
+ * }}
26
+ * onSkip={() => { ... }} // optional — hide skip button if not provided
27
+ * />
28
+ * );
29
+ * }
30
+ *
31
+ * What the screen does for each missing connector, in order:
32
+ * 1. Shows connector name / description (fetched from API).
33
+ * 2. "Connect" button → calls createConnection → shows an in-app WebView
34
+ * with the OAuth consent URL.
35
+ * 3. "Done" button in WebView header → calls checkNow() to confirm status.
36
+ * 4. On Connected → advances to the next missing connector.
37
+ * 5. When all connectors are resolved → calls onAllConnected.
38
+ *
39
+ * The component has zero production dependencies beyond react-native.
40
+ * It purposely does NOT depend on expo-router or any navigation library —
41
+ * the consuming app decides how to render it (full-screen, modal, etc.).
42
+ */
43
+
44
+ import React, { useState, useEffect, useCallback } from 'react';
45
+ import { View, Text, TouchableOpacity, ActivityIndicator, StyleSheet, ScrollView } from 'react-native';
46
+ import * as WebBrowser from 'expo-web-browser';
47
+ import * as Linking from 'expo-linking';
48
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
49
+ import { useConnectionSetup } from "../connections/useConnectionSetup.js";
50
+ import { fetchConnectorMetadata, fetchConnectionsForConnector } from "../platform/PowerPlatformConnectionApi.js";
51
+ import { powerPlatformScope } from "../platform/PowerPlatformEnvironmentApi.js";
52
+ import { ConnectionStatus } from "../connections/types.js";
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Types
56
+ // ---------------------------------------------------------------------------
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Internal helper: ConnectorSetupStep
60
+ // Sets up a single connector and reports back via onConnected.
61
+ // ---------------------------------------------------------------------------
62
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
63
+ function ConnectorSetupStep({
64
+ connectorId,
65
+ apiId,
66
+ environmentId,
67
+ region,
68
+ acquireToken,
69
+ isvClientId,
70
+ onConnected,
71
+ renderOAuthWebView
72
+ }) {
73
+ // Derive the OAuth redirect URL from the app's registered scheme at runtime.
74
+ // expo-linking.createURL resolves to e.g. "myapp://oauth-callback" using
75
+ // whatever scheme the host app has registered — works universally without
76
+ // any configuration by the consuming app.
77
+ const oauthRedirectUrl = Linking.createURL('oauth-callback');
78
+
79
+ // Local UI state for the metadata-loading phase. Kept here (not in
80
+ // useConnectionSetup) so the hook stays focused on connection flow only.
81
+ const [isLoadingMetadata, setIsLoadingMetadata] = useState(true);
82
+ const [metadata, setMetadata] = useState(null);
83
+ const [showOAuthModal, setShowOAuthModal] = useState(false);
84
+ // Track if there's already a connected instance the user can reuse.
85
+ const [existingConnected, setExistingConnected] = useState(null);
86
+ const setup = useConnectionSetup({
87
+ connectorId,
88
+ environmentId,
89
+ region,
90
+ acquireToken,
91
+ isvClientId,
92
+ onConnected: instance => {
93
+ setShowOAuthModal(false);
94
+ onConnected(apiId, instance);
95
+ },
96
+ oauthRedirectUrl,
97
+ polling: {
98
+ intervalMs: 2000,
99
+ timeoutMs: 120_000
100
+ }
101
+ });
102
+
103
+ // Fetch connector metadata and check for existing non-connected connections.
104
+ useEffect(() => {
105
+ let cancelled = false;
106
+ async function load() {
107
+ const token = await acquireToken([powerPlatformScope(region)]);
108
+ if (!token || cancelled) {
109
+ if (!cancelled) {
110
+ setMetadata({
111
+ displayName: connectorId,
112
+ iconUri: '',
113
+ description: ''
114
+ });
115
+ setIsLoadingMetadata(false);
116
+ }
117
+ return;
118
+ }
119
+ const [meta, instances] = await Promise.all([fetchConnectorMetadata(connectorId, environmentId, region, token), fetchConnectionsForConnector(connectorId, environmentId, region, token)]);
120
+ if (cancelled) return;
121
+ setMetadata(meta ?? {
122
+ displayName: connectorId,
123
+ iconUri: '',
124
+ description: ''
125
+ });
126
+
127
+ // If there's already a Connected instance the disambiguation didn't pick
128
+ // (e.g., appeared after the initial load), use it directly.
129
+ const connected = instances.find(i => i.status === ConnectionStatus.Connected);
130
+ if (connected) setExistingConnected(connected);
131
+ setIsLoadingMetadata(false);
132
+ }
133
+ void load();
134
+ return () => {
135
+ cancelled = true;
136
+ };
137
+ // eslint-disable-next-line react-hooks/exhaustive-deps
138
+ }, [connectorId]);
139
+ const handleUseExisting = useCallback(() => {
140
+ if (existingConnected) {
141
+ onConnected(apiId, existingConnected);
142
+ }
143
+ }, [existingConnected, apiId, onConnected]);
144
+ const handleConnect = useCallback(async () => {
145
+ await setup.startCreate();
146
+ }, [setup]);
147
+
148
+ // Open OAuth UI when consentUrl becomes available (set by startCreate in the hook).
149
+ useEffect(() => {
150
+ if (setup.phase === 'oauth' && setup.consentUrl) {
151
+ if (renderOAuthWebView) {
152
+ setShowOAuthModal(true);
153
+ } else {
154
+ // Default: open an in-app browser session.
155
+ // openAuthSessionAsync auto-dismisses when the OS intercepts the
156
+ // custom-scheme redirect (e.g. expo-app://oauth-callback).
157
+ void (async () => {
158
+ const result = await WebBrowser.openAuthSessionAsync(setup.consentUrl, oauthRedirectUrl);
159
+ // Extract a CommitTokenCode from the redirect URL if present.
160
+ let code;
161
+ if (result.type === 'success' && result.url) {
162
+ const params = new URL(result.url).searchParams;
163
+ const raw = params.get('code') ?? '';
164
+ // CommitTokenCode is a 32-char lowercase hex string.
165
+ if (/^[0-9a-f]{32}$/i.test(raw)) {
166
+ code = raw;
167
+ }
168
+ }
169
+ await setup.completeConsent(code);
170
+ })();
171
+ }
172
+ }
173
+ }, [setup.phase, setup.consentUrl, renderOAuthWebView, oauthRedirectUrl]);
174
+ const handleWebViewDismiss = useCallback(async () => {
175
+ setShowOAuthModal(false);
176
+ await setup.checkNow();
177
+ }, [setup]);
178
+ const displayName = metadata?.displayName ?? connectorId;
179
+ const description = metadata?.description ?? '';
180
+ if (isLoadingMetadata) {
181
+ return /*#__PURE__*/_jsx(View, {
182
+ style: styles.connectorCard,
183
+ children: /*#__PURE__*/_jsx(View, {
184
+ style: styles.metadataLoading,
185
+ children: /*#__PURE__*/_jsx(ActivityIndicator, {
186
+ size: "small"
187
+ })
188
+ })
189
+ });
190
+ }
191
+ return /*#__PURE__*/_jsxs(View, {
192
+ style: styles.connectorCard,
193
+ children: [/*#__PURE__*/_jsx(Text, {
194
+ style: styles.connectorName,
195
+ children: displayName
196
+ }), description ? /*#__PURE__*/_jsx(Text, {
197
+ style: styles.connectorDescription,
198
+ children: description
199
+ }) : null, setup.phase === 'idle' && /*#__PURE__*/_jsx(_Fragment, {
200
+ children: existingConnected ? /*#__PURE__*/_jsxs(View, {
201
+ style: styles.buttonRow,
202
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
203
+ style: [styles.button, styles.buttonSecondary],
204
+ onPress: handleUseExisting,
205
+ children: /*#__PURE__*/_jsx(Text, {
206
+ style: styles.buttonTextSecondary,
207
+ children: "Use existing"
208
+ })
209
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
210
+ style: styles.button,
211
+ onPress: handleConnect,
212
+ children: /*#__PURE__*/_jsx(Text, {
213
+ style: styles.buttonText,
214
+ children: "Create new"
215
+ })
216
+ })]
217
+ }) : /*#__PURE__*/_jsx(TouchableOpacity, {
218
+ style: styles.button,
219
+ onPress: handleConnect,
220
+ children: /*#__PURE__*/_jsx(Text, {
221
+ style: styles.buttonText,
222
+ children: "Connect"
223
+ })
224
+ })
225
+ }), (setup.phase === 'creating' || setup.phase === 'confirming') && /*#__PURE__*/_jsxs(View, {
226
+ style: styles.statusRow,
227
+ children: [/*#__PURE__*/_jsx(ActivityIndicator, {
228
+ size: "small",
229
+ color: "#0078D4"
230
+ }), /*#__PURE__*/_jsx(Text, {
231
+ style: styles.statusText,
232
+ children: setup.phase === 'creating' ? 'Creating connection…' : 'Confirming…'
233
+ })]
234
+ }), setup.phase === 'oauth' && /*#__PURE__*/_jsxs(View, {
235
+ style: styles.statusRow,
236
+ children: [/*#__PURE__*/_jsx(ActivityIndicator, {
237
+ size: "small",
238
+ color: "#0078D4"
239
+ }), /*#__PURE__*/_jsx(Text, {
240
+ style: styles.statusText,
241
+ children: "Waiting for sign-in\u2026"
242
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
243
+ onPress: handleWebViewDismiss,
244
+ children: /*#__PURE__*/_jsx(Text, {
245
+ style: styles.linkText,
246
+ children: "Done"
247
+ })
248
+ })]
249
+ }), setup.phase === 'connected' && /*#__PURE__*/_jsx(View, {
250
+ style: styles.statusRow,
251
+ children: /*#__PURE__*/_jsx(Text, {
252
+ style: styles.successText,
253
+ children: "\u2713 Connected"
254
+ })
255
+ }), setup.phase === 'error' && /*#__PURE__*/_jsxs(View, {
256
+ children: [/*#__PURE__*/_jsx(Text, {
257
+ style: styles.errorText,
258
+ children: setup.errorMessage ?? 'Connection failed'
259
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
260
+ style: [styles.button, styles.buttonDanger],
261
+ onPress: setup.reset,
262
+ children: /*#__PURE__*/_jsx(Text, {
263
+ style: styles.buttonText,
264
+ children: "Retry"
265
+ })
266
+ })]
267
+ }), showOAuthModal && setup.consentUrl && renderOAuthWebView ? renderOAuthWebView(setup.consentUrl, handleWebViewDismiss) : null]
268
+ });
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Main screen component
273
+ // ---------------------------------------------------------------------------
274
+
275
+ /**
276
+ * Full-screen component that steps the user through creating each missing
277
+ * connection before the Power Apps player is launched.
278
+ *
279
+ * The screen works through missing connectors sequentially. Once all are
280
+ * resolved `onAllConnected` fires and the parent app should unmount this
281
+ * component and proceed to launch the app.
282
+ */
283
+ export function ConnectionSetupScreen({
284
+ missingConnectors,
285
+ schemaMap,
286
+ environmentId,
287
+ region = 'prod',
288
+ acquireToken,
289
+ isvClientId,
290
+ onAllConnected,
291
+ onSkip,
292
+ renderOAuthWebView,
293
+ title = 'Set up connections'
294
+ }) {
295
+ // resolvedIds accumulates as each connector is connected.
296
+ const [resolvedIds, setResolvedIds] = useState({});
297
+ const insets = useSafeAreaInsets();
298
+
299
+ // Build connectorId (short name) for each missing entry from the schemaMap.
300
+ // schemaMap keys are data source names; we need to find the ConnectorSchema
301
+ // whose apiId matches each missing connector's apiId.
302
+ const connectorIdByApiId = React.useMemo(() => {
303
+ const map = {};
304
+ for (const schema of Object.values(schemaMap)) {
305
+ // The schemaMap doesn't directly store apiId — we reconstruct it.
306
+ // Convention: connectorId = schema.name (e.g. "shared_office365users")
307
+ // apiId = "/providers/Microsoft.PowerApps/apis/shared_office365users"
308
+ const derivedApiId = `/providers/Microsoft.PowerApps/apis/${schema.name}`;
309
+ map[derivedApiId] = schema.name;
310
+ }
311
+ return map;
312
+ }, [schemaMap]);
313
+ const handleConnected = useCallback((apiId, instance) => {
314
+ setResolvedIds(prev => ({
315
+ ...prev,
316
+ [apiId]: instance.id
317
+ }));
318
+ }, []);
319
+
320
+ // Fire onAllConnected once every connector has been resolved.
321
+ // Done in an effect so it never runs inside a state updater (which would
322
+ // trigger the "Cannot update a component while rendering" React error).
323
+ useEffect(() => {
324
+ if (missingConnectors.length === 0) return;
325
+ const allResolved = missingConnectors.every(mc => resolvedIds[mc.apiId] !== undefined);
326
+ if (allResolved) {
327
+ onAllConnected(resolvedIds);
328
+ }
329
+ }, [resolvedIds, missingConnectors, onAllConnected]);
330
+ const resolvedCount = Object.keys(resolvedIds).length;
331
+ const total = missingConnectors.length;
332
+ return /*#__PURE__*/_jsxs(View, {
333
+ style: [styles.container, {
334
+ paddingTop: insets.top,
335
+ paddingBottom: insets.bottom
336
+ }],
337
+ children: [/*#__PURE__*/_jsxs(ScrollView, {
338
+ contentContainerStyle: styles.scrollContent,
339
+ children: [/*#__PURE__*/_jsx(Text, {
340
+ style: styles.title,
341
+ children: title
342
+ }), /*#__PURE__*/_jsxs(Text, {
343
+ style: styles.subtitle,
344
+ children: ["This app needs ", total === 1 ? 'a connection' : `${total} connections`, " to work. Sign in to each service below to continue."]
345
+ }), resolvedCount > 0 && /*#__PURE__*/_jsxs(Text, {
346
+ style: styles.progressText,
347
+ children: [resolvedCount, " of ", total, " connected"]
348
+ }), missingConnectors.map(mc => {
349
+ const connectorId = connectorIdByApiId[mc.apiId];
350
+ if (!connectorId) {
351
+ // Unknown connector — skip rendering.
352
+ return null;
353
+ }
354
+ const isResolved = resolvedIds[mc.apiId] !== undefined;
355
+ if (isResolved) {
356
+ return /*#__PURE__*/_jsx(View, {
357
+ style: [styles.connectorCard, styles.connectorCardResolved],
358
+ children: /*#__PURE__*/_jsxs(Text, {
359
+ style: styles.successText,
360
+ children: ["\u2713 ", connectorId.replace('shared_', '')]
361
+ })
362
+ }, mc.apiId);
363
+ }
364
+ return /*#__PURE__*/_jsx(ConnectorSetupStep, {
365
+ connectorId: connectorId,
366
+ apiId: mc.apiId,
367
+ environmentId: environmentId,
368
+ region: region,
369
+ acquireToken: acquireToken,
370
+ isvClientId: isvClientId,
371
+ onConnected: handleConnected,
372
+ renderOAuthWebView: renderOAuthWebView
373
+ }, mc.apiId);
374
+ })]
375
+ }), onSkip ? /*#__PURE__*/_jsx(View, {
376
+ style: styles.footer,
377
+ children: /*#__PURE__*/_jsx(TouchableOpacity, {
378
+ onPress: onSkip,
379
+ style: styles.skipButton,
380
+ children: /*#__PURE__*/_jsx(Text, {
381
+ style: styles.skipText,
382
+ children: "Skip for now"
383
+ })
384
+ })
385
+ }) : null]
386
+ });
387
+ }
388
+
389
+ // ---------------------------------------------------------------------------
390
+ // Styles
391
+ // ---------------------------------------------------------------------------
392
+
393
+ const styles = StyleSheet.create({
394
+ container: {
395
+ flex: 1,
396
+ backgroundColor: '#F8F8F8'
397
+ },
398
+ scrollContent: {
399
+ padding: 20,
400
+ paddingBottom: 40
401
+ },
402
+ title: {
403
+ fontSize: 24,
404
+ fontWeight: '700',
405
+ color: '#1A1A1A',
406
+ marginBottom: 8
407
+ },
408
+ subtitle: {
409
+ fontSize: 15,
410
+ color: '#555',
411
+ marginBottom: 24,
412
+ lineHeight: 22
413
+ },
414
+ progressText: {
415
+ fontSize: 13,
416
+ color: '#0078D4',
417
+ fontWeight: '600',
418
+ marginBottom: 16
419
+ },
420
+ connectorCard: {
421
+ backgroundColor: '#FFF',
422
+ borderRadius: 12,
423
+ padding: 16,
424
+ marginBottom: 12,
425
+ minHeight: 80,
426
+ shadowColor: '#000',
427
+ shadowOffset: {
428
+ width: 0,
429
+ height: 1
430
+ },
431
+ shadowOpacity: 0.06,
432
+ shadowRadius: 4,
433
+ elevation: 2
434
+ },
435
+ connectorCardResolved: {
436
+ backgroundColor: '#F0FFF4',
437
+ borderColor: '#34A853',
438
+ borderWidth: 1
439
+ },
440
+ connectorName: {
441
+ fontSize: 16,
442
+ fontWeight: '600',
443
+ color: '#1A1A1A',
444
+ marginBottom: 4
445
+ },
446
+ connectorDescription: {
447
+ fontSize: 13,
448
+ color: '#666',
449
+ marginBottom: 12,
450
+ lineHeight: 18
451
+ },
452
+ buttonRow: {
453
+ flexDirection: 'row',
454
+ gap: 8
455
+ },
456
+ button: {
457
+ flex: 1,
458
+ backgroundColor: '#0078D4',
459
+ borderRadius: 8,
460
+ paddingVertical: 10,
461
+ alignItems: 'center',
462
+ marginTop: 8
463
+ },
464
+ buttonSecondary: {
465
+ backgroundColor: '#EFF6FC'
466
+ },
467
+ buttonDanger: {
468
+ backgroundColor: '#C4262E'
469
+ },
470
+ buttonText: {
471
+ color: '#FFF',
472
+ fontWeight: '600',
473
+ fontSize: 14
474
+ },
475
+ buttonTextSecondary: {
476
+ color: '#0078D4',
477
+ fontWeight: '600',
478
+ fontSize: 14
479
+ },
480
+ statusRow: {
481
+ flexDirection: 'row',
482
+ alignItems: 'center',
483
+ gap: 8,
484
+ marginTop: 8
485
+ },
486
+ statusText: {
487
+ fontSize: 14,
488
+ color: '#555',
489
+ flex: 1
490
+ },
491
+ linkText: {
492
+ fontSize: 14,
493
+ color: '#0078D4',
494
+ fontWeight: '600'
495
+ },
496
+ successText: {
497
+ fontSize: 14,
498
+ color: '#34A853',
499
+ fontWeight: '600'
500
+ },
501
+ errorText: {
502
+ fontSize: 13,
503
+ color: '#C4262E',
504
+ marginBottom: 8
505
+ },
506
+ footer: {
507
+ padding: 16,
508
+ borderTopWidth: StyleSheet.hairlineWidth,
509
+ borderTopColor: '#E0E0E0',
510
+ backgroundColor: '#FFF'
511
+ },
512
+ skipButton: {
513
+ alignItems: 'center',
514
+ paddingVertical: 12
515
+ },
516
+ skipText: {
517
+ fontSize: 15,
518
+ color: '#888'
519
+ },
520
+ metadataLoading: {
521
+ flex: 1,
522
+ alignItems: 'center',
523
+ justifyContent: 'center',
524
+ minHeight: 48
525
+ }
526
+ });
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+
3
+ /*!
4
+ * power-apps-native-host — connection disambiguation logic
5
+ *
6
+ * Pure function that matches power.config.json connectionReferences to the
7
+ * connection instances available in the user's Power Platform environment.
8
+ *
9
+ * Mirrors the core of ConnectionPipeline.matchConnectionsToUsages in the web
10
+ * host but is self-contained — it does not require IConnectionService or any
11
+ * other web-host dependency.
12
+ */
13
+
14
+ import { ConnectionStatus } from "./types.js";
15
+ /**
16
+ * Disambiguate a set of connection references against the user's available
17
+ * connection instances.
18
+ *
19
+ * Rules (in priority order):
20
+ * 1. If exactly one `Connected` connection matches the reference apiId → resolved.
21
+ * 2. If zero connections match (or none are Connected) → missing.
22
+ * 3. If multiple Connected connections match → ambiguous (needs user pick).
23
+ *
24
+ * @param connectionReferences The `connectionReferences` object from power.config.json,
25
+ * keyed by local reference ID.
26
+ * @param allConnectionsByApiId All connection instances fetched from the
27
+ * Power Platform connectivity API, keyed by
28
+ * connector apiId.
29
+ */
30
+ export function disambiguateConnections(connectionReferences, allConnectionsByApiId) {
31
+ const resolutions = [];
32
+ for (const [localRefId, ref] of Object.entries(connectionReferences)) {
33
+ const apiId = ref.id;
34
+ const all = allConnectionsByApiId[apiId] ?? [];
35
+ const connected = all.filter(c => c.status === ConnectionStatus.Connected);
36
+ let result;
37
+ if (connected.length === 1) {
38
+ result = {
39
+ kind: 'resolved',
40
+ connectionId: connected[0].id
41
+ };
42
+ } else if (connected.length === 0) {
43
+ result = {
44
+ kind: 'missing'
45
+ };
46
+ } else {
47
+ // Multiple Connected connections — needs user pick.
48
+ result = {
49
+ kind: 'ambiguous',
50
+ candidates: connected
51
+ };
52
+ }
53
+ resolutions.push({
54
+ localRefId,
55
+ apiId,
56
+ dataSources: ref.dataSources,
57
+ result
58
+ });
59
+ }
60
+ return resolutions;
61
+ }
62
+
63
+ /**
64
+ * Extract the connection UUID from a raw connection name returned by the API.
65
+ *
66
+ * The API returns names like:
67
+ * "shared_office365users/abc12345def6789012345678901234ab" (connector-prefixed)
68
+ * "abc12345def6789012345678901234ab" (bare UUID, no dashes)
69
+ * "abc12345-def6-7890-1234-5678901234ab" (UUID with dashes)
70
+ *
71
+ * Returns the 32-char hex ID (dashes stripped) or the raw string if no UUID
72
+ * pattern is found.
73
+ */
74
+ export function extractConnectionId(rawName) {
75
+ // Match a standard UUID (with or without dashes) at the end of the string.
76
+ const match = rawName.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i);
77
+ if (match) {
78
+ return match[1].replace(/-/g, '');
79
+ }
80
+ // Some connections use a 32-char hex ID without dashes, after a slash.
81
+ const slashIdx = rawName.lastIndexOf('/');
82
+ return slashIdx >= 0 ? rawName.slice(slashIdx + 1) : rawName;
83
+ }