@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.
- package/LICENSE +112 -0
- package/README.md +59 -0
- package/app.plugin.js +4 -0
- package/lib/NOTICE.txt +1659 -0
- package/lib/commonjs/ErrorBoundary.js +128 -0
- package/lib/commonjs/auth/AuthContext.js +281 -0
- package/lib/commonjs/bridge/NativePowerAppsBridge.js +279 -0
- package/lib/commonjs/bridge/setupNativeHost.js +65 -0
- package/lib/commonjs/connections/ConnectionSetupScreen.js +531 -0
- package/lib/commonjs/connections/disambiguate.js +88 -0
- package/lib/commonjs/connections/types.js +58 -0
- package/lib/commonjs/connections/useConnectionSetup.js +236 -0
- package/lib/commonjs/controls/FilePicker.js +183 -0
- package/lib/commonjs/controls/ImagePicker.js +236 -0
- package/lib/commonjs/index.js +246 -0
- package/lib/commonjs/logger.js +27 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/platform/PowerPlatformConnectionApi.js +606 -0
- package/lib/commonjs/platform/PowerPlatformEnvironmentApi.js +194 -0
- package/lib/commonjs/platform/resolveConnections.js +264 -0
- package/lib/commonjs/platform/useConnectionRefs.js +270 -0
- package/lib/commonjs/provider/PowerAppsHostProvider.js +336 -0
- package/lib/commonjs/provider/PowerAppsProvider.js +351 -0
- package/lib/commonjs/telemetry/index.js +70 -0
- package/lib/commonjs/theme/ThemeProvider.js +80 -0
- package/lib/commonjs/theme/tokens.js +50 -0
- package/lib/commonjs/utils/dataverseUtils.js +98 -0
- package/lib/commonjs/utils/isDevPlayer.js +27 -0
- package/lib/module/ErrorBoundary.js +122 -0
- package/lib/module/auth/AuthContext.js +276 -0
- package/lib/module/bridge/NativePowerAppsBridge.js +273 -0
- package/lib/module/bridge/setupNativeHost.js +62 -0
- package/lib/module/connections/ConnectionSetupScreen.js +526 -0
- package/lib/module/connections/disambiguate.js +83 -0
- package/lib/module/connections/types.js +60 -0
- package/lib/module/connections/useConnectionSetup.js +233 -0
- package/lib/module/controls/FilePicker.js +178 -0
- package/lib/module/controls/ImagePicker.js +231 -0
- package/lib/module/index.js +41 -0
- package/lib/module/logger.js +22 -0
- package/lib/module/package.json +1 -0
- package/lib/module/platform/PowerPlatformConnectionApi.js +595 -0
- package/lib/module/platform/PowerPlatformEnvironmentApi.js +186 -0
- package/lib/module/platform/resolveConnections.js +259 -0
- package/lib/module/platform/useConnectionRefs.js +267 -0
- package/lib/module/provider/PowerAppsHostProvider.js +332 -0
- package/lib/module/provider/PowerAppsProvider.js +345 -0
- package/lib/module/telemetry/index.js +55 -0
- package/lib/module/theme/ThemeProvider.js +63 -0
- package/lib/module/theme/tokens.js +46 -0
- package/lib/module/utils/dataverseUtils.js +90 -0
- package/lib/module/utils/isDevPlayer.js +23 -0
- package/lib/typescript/commonjs/ErrorBoundary.d.ts +30 -0
- package/lib/typescript/commonjs/auth/AuthContext.d.ts +57 -0
- package/lib/typescript/commonjs/bridge/NativePowerAppsBridge.d.ts +112 -0
- package/lib/typescript/commonjs/bridge/setupNativeHost.d.ts +39 -0
- package/lib/typescript/commonjs/connections/ConnectionSetupScreen.d.ts +107 -0
- package/lib/typescript/commonjs/connections/disambiguate.d.ts +43 -0
- package/lib/typescript/commonjs/connections/types.d.ts +91 -0
- package/lib/typescript/commonjs/connections/useConnectionSetup.d.ts +96 -0
- package/lib/typescript/commonjs/controls/FilePicker.d.ts +65 -0
- package/lib/typescript/commonjs/controls/ImagePicker.d.ts +76 -0
- package/lib/typescript/commonjs/index.d.ts +42 -0
- package/lib/typescript/commonjs/logger.d.ts +11 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/platform/PowerPlatformConnectionApi.d.ts +205 -0
- package/lib/typescript/commonjs/platform/PowerPlatformEnvironmentApi.d.ts +108 -0
- package/lib/typescript/commonjs/platform/resolveConnections.d.ts +86 -0
- package/lib/typescript/commonjs/platform/useConnectionRefs.d.ts +126 -0
- package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +123 -0
- package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +148 -0
- package/lib/typescript/commonjs/telemetry/index.d.ts +48 -0
- package/lib/typescript/commonjs/theme/ThemeProvider.d.ts +30 -0
- package/lib/typescript/commonjs/theme/tokens.d.ts +27 -0
- package/lib/typescript/commonjs/utils/dataverseUtils.d.ts +39 -0
- package/lib/typescript/commonjs/utils/isDevPlayer.d.ts +19 -0
- package/lib/typescript/module/ErrorBoundary.d.ts +30 -0
- package/lib/typescript/module/auth/AuthContext.d.ts +57 -0
- package/lib/typescript/module/bridge/NativePowerAppsBridge.d.ts +112 -0
- package/lib/typescript/module/bridge/setupNativeHost.d.ts +39 -0
- package/lib/typescript/module/connections/ConnectionSetupScreen.d.ts +107 -0
- package/lib/typescript/module/connections/disambiguate.d.ts +43 -0
- package/lib/typescript/module/connections/types.d.ts +91 -0
- package/lib/typescript/module/connections/useConnectionSetup.d.ts +96 -0
- package/lib/typescript/module/controls/FilePicker.d.ts +65 -0
- package/lib/typescript/module/controls/ImagePicker.d.ts +76 -0
- package/lib/typescript/module/index.d.ts +42 -0
- package/lib/typescript/module/logger.d.ts +11 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/platform/PowerPlatformConnectionApi.d.ts +205 -0
- package/lib/typescript/module/platform/PowerPlatformEnvironmentApi.d.ts +108 -0
- package/lib/typescript/module/platform/resolveConnections.d.ts +86 -0
- package/lib/typescript/module/platform/useConnectionRefs.d.ts +126 -0
- package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +123 -0
- package/lib/typescript/module/provider/PowerAppsProvider.d.ts +148 -0
- package/lib/typescript/module/telemetry/index.d.ts +48 -0
- package/lib/typescript/module/theme/ThemeProvider.d.ts +30 -0
- package/lib/typescript/module/theme/tokens.d.ts +27 -0
- package/lib/typescript/module/utils/dataverseUtils.d.ts +39 -0
- package/lib/typescript/module/utils/isDevPlayer.d.ts +19 -0
- package/package.json +119 -0
- package/scripts/build-ios-base.js +96 -0
- package/scripts/bundle.js +95 -0
- 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
|
+
}
|