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