@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,122 @@
1
+ "use strict";
2
+
3
+ import React from 'react';
4
+ import { ScrollView, StyleSheet, Text } from 'react-native';
5
+ import { debugLog } from "./logger.js";
6
+ import { getAppLogger, HostOperationNames } from "./telemetry/index.js";
7
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
8
+ /**
9
+ * Top-level React error boundary.
10
+ *
11
+ * Catches any unhandled JS exception thrown during rendering / lifecycle and
12
+ * displays a diagnostic screen. All error details are written to the native
13
+ * log via debugLog so diagnostics stay consistent with the host package logger.
14
+ *
15
+ * Place this component at the very root of the component tree so it captures
16
+ * failures in any screen.
17
+ *
18
+ * Exported from power-apps-native-host so it can be used in the template and
19
+ * in any custom app without duplicating the implementation.
20
+ */
21
+ export class ErrorBoundary extends React.Component {
22
+ state = {
23
+ hasError: false,
24
+ error: null,
25
+ componentStack: null
26
+ };
27
+ static getDerivedStateFromError(error) {
28
+ return {
29
+ hasError: true,
30
+ error
31
+ };
32
+ }
33
+ componentDidCatch(error, info) {
34
+ this.setState({
35
+ componentStack: info.componentStack ?? null
36
+ });
37
+ debugLog('[PAHost][ErrorBoundary] Unhandled JS error:', error.message);
38
+ debugLog('[PAHost][ErrorBoundary] Error stack:', error.stack ?? '(no stack)');
39
+ debugLog('[PAHost][ErrorBoundary] Component stack:', info.componentStack ?? '(no component stack)');
40
+ getAppLogger().trackErrorEvent(HostOperationNames.RuntimeUnhandledJsError, {
41
+ errorName: error.constructor.name,
42
+ hasStack: Boolean(error.stack),
43
+ hasComponentStack: Boolean(info.componentStack)
44
+ });
45
+ }
46
+ render() {
47
+ if (!this.state.hasError) {
48
+ return this.props.children;
49
+ }
50
+ const {
51
+ error,
52
+ componentStack
53
+ } = this.state;
54
+ return /*#__PURE__*/_jsxs(ScrollView, {
55
+ contentContainerStyle: styles.container,
56
+ children: [/*#__PURE__*/_jsx(Text, {
57
+ style: styles.title,
58
+ children: "Something went wrong"
59
+ }), /*#__PURE__*/_jsxs(Text, {
60
+ style: styles.subtitle,
61
+ children: ["Check the native log for full details:", '\n', "adb logcat -s ReactNativeJS:V"]
62
+ }), /*#__PURE__*/_jsx(Text, {
63
+ style: styles.label,
64
+ children: "Error"
65
+ }), /*#__PURE__*/_jsx(Text, {
66
+ style: styles.mono,
67
+ children: error?.message ?? 'Unknown error'
68
+ }), error?.stack ? /*#__PURE__*/_jsxs(_Fragment, {
69
+ children: [/*#__PURE__*/_jsx(Text, {
70
+ style: styles.label,
71
+ children: "JS Stack"
72
+ }), /*#__PURE__*/_jsx(Text, {
73
+ style: styles.mono,
74
+ children: error.stack
75
+ })]
76
+ }) : null, componentStack ? /*#__PURE__*/_jsxs(_Fragment, {
77
+ children: [/*#__PURE__*/_jsx(Text, {
78
+ style: styles.label,
79
+ children: "Component Stack"
80
+ }), /*#__PURE__*/_jsx(Text, {
81
+ style: styles.mono,
82
+ children: componentStack
83
+ })]
84
+ }) : null]
85
+ });
86
+ }
87
+ }
88
+ const styles = StyleSheet.create({
89
+ container: {
90
+ flexGrow: 1,
91
+ padding: 20,
92
+ paddingTop: 60,
93
+ backgroundColor: '#1a1a1a'
94
+ },
95
+ title: {
96
+ fontSize: 22,
97
+ fontWeight: '700',
98
+ color: '#ff4d4d',
99
+ marginBottom: 8
100
+ },
101
+ subtitle: {
102
+ fontSize: 12,
103
+ color: '#888',
104
+ marginBottom: 20,
105
+ lineHeight: 18
106
+ },
107
+ label: {
108
+ fontSize: 11,
109
+ fontWeight: '600',
110
+ color: '#aaa',
111
+ marginTop: 16,
112
+ marginBottom: 4,
113
+ textTransform: 'uppercase',
114
+ letterSpacing: 0.8
115
+ },
116
+ mono: {
117
+ fontSize: 12,
118
+ color: '#e8e8e8',
119
+ fontFamily: 'monospace',
120
+ lineHeight: 18
121
+ }
122
+ });
@@ -0,0 +1,276 @@
1
+ "use strict";
2
+
3
+ /*!
4
+ * power-apps-native-host — AuthContext
5
+ *
6
+ * A generic MSAL auth provider built on power-apps-native-auth's
7
+ * PublicClientApplication. Provides AuthProvider, useAuth, and the
8
+ * AuthState / AuthProviderProps types.
9
+ *
10
+ * Usage in the consuming Expo app:
11
+ *
12
+ * <AuthProvider msalConfig={authConfig.msal}>
13
+ * <PowerAppsHostProvider acquireToken={acquireToken} ...>
14
+ * {children}
15
+ * </PowerAppsHostProvider>
16
+ * </AuthProvider>
17
+ */
18
+
19
+ import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
20
+ import { Platform } from 'react-native';
21
+ import { PublicClientApplication, registerAndEnrollAccount, deRegisterAndUnenrollAccount, computeMsalRedirectUri, getWrapConfig } from '@microsoft/power-apps-native-auth';
22
+ import { getAppLogger, HostOperationNames } from "../telemetry/index.js";
23
+ import { debugLog, debugWarn } from "../logger.js";
24
+
25
+ // The Power Platform API (api.powerplatform.com) exposes its own scopes, not raw
26
+ // OIDC scopes. Requesting 'openid'/'profile' alone causes AAD to decline.
27
+ // '/.default' acquires a token covering all pre-consented permissions and
28
+ // implicitly includes openid + profile + offline_access for the sign-in flow.
29
+ import { jsx as _jsx } from "react/jsx-runtime";
30
+ const SIGN_IN_SCOPES = ['https://api.powerplatform.com/.default'];
31
+ function getSafeErrorName(error) {
32
+ return error instanceof Error ? error.constructor.name : typeof error;
33
+ }
34
+ const AuthContext = /*#__PURE__*/createContext(null);
35
+ export function AuthProvider({
36
+ msalConfig,
37
+ intuneConfig,
38
+ children
39
+ }) {
40
+ const [isAuthReady, setIsAuthReady] = useState(false);
41
+ const [isLoading, setIsLoading] = useState(true);
42
+ const [account, setAccount] = useState(null);
43
+ const [error, setError] = useState(null);
44
+
45
+ // useBroker, useEmbeddedWebView, and intune.enabled all default to !__DEV__ / __DEV__:
46
+ // dev client (__DEV__=true) → broker off, embedded WebView on, MAM enrollment off
47
+ // release build (__DEV__=false) → broker on, embedded WebView off, MAM enrollment on
48
+ // Callers can override via msalConfig.useBroker / msalConfig.useEmbeddedWebView / intuneConfig.enabled.
49
+ const useBroker = msalConfig.useBroker ?? !__DEV__;
50
+ const useEmbeddedWebView = msalConfig.useEmbeddedWebView ?? __DEV__;
51
+ const intuneEnabled = intuneConfig?.enabled ?? !__DEV__;
52
+
53
+ // In release builds, read clientId and tenantId from native platform sources
54
+ // patched by the production wrap pipeline (Android: string resources;
55
+ // iOS: Info.plist). Fall back to auth.config.json only when the native value
56
+ // is absent or is still the build-time placeholder (base package not yet
57
+ // wrapped).
58
+ const effectiveClientId = (() => {
59
+ if (__DEV__) return msalConfig.clientId;
60
+ try {
61
+ const {
62
+ clientId
63
+ } = getWrapConfig();
64
+ return clientId && !clientId.includes('_TAG') ? clientId : msalConfig.clientId;
65
+ } catch {
66
+ return msalConfig.clientId;
67
+ }
68
+ })();
69
+ const effectiveTenantId = (() => {
70
+ if (__DEV__) return msalConfig.tenantId;
71
+ try {
72
+ const {
73
+ tenantId
74
+ } = getWrapConfig();
75
+ return tenantId && !tenantId.includes('_TAG') ? tenantId : msalConfig.tenantId;
76
+ } catch {
77
+ return msalConfig.tenantId;
78
+ }
79
+ })();
80
+
81
+ // When broker is off (Android dev/embedded-WebView mode) the msauth:// redirect
82
+ // URI is not registered in AAD and will cause "multiple apps listening" errors
83
+ // because every dev client on the device claims the msauth.<package> intent
84
+ // filter. The nativeclient URI works for embedded-WebView sign-in and is
85
+ // registered for all AAD tenants.
86
+ //
87
+ // iOS MSAL rejects http(s) schemes for native flows (the "multiple apps
88
+ // listening" clash doesn't exist on iOS — msauth.<bundleId> is unique per
89
+ // bundle id), so iOS always uses computeMsalRedirectUri(). The dev-client
90
+ // consumer must register `msauth.<bundleId>://auth` in AAD and ensure the
91
+ // matching URL scheme is in CFBundleURLTypes (the production wrap pipeline
92
+ // injects it at release time).
93
+ //
94
+ // In release builds useBroker=true so all platforms compute the redirect
95
+ // URI natively from the app's own package name + signing cert.
96
+ const effectiveRedirectUri = !useBroker && Platform.OS === 'android' ? 'https://login.microsoftonline.com/common/oauth2/nativeclient' : computeMsalRedirectUri();
97
+
98
+ // No clientId → no-auth mode (e.g. dev-player before MSAL is configured).
99
+ // Skip MSAL entirely; treat the session as already signed in so the app
100
+ // renders without requiring credentials.
101
+ const noAuthMode = !effectiveClientId;
102
+ const pcaRef = useRef(null);
103
+ useEffect(() => {
104
+ if (noAuthMode) {
105
+ setIsAuthReady(true);
106
+ setIsLoading(false);
107
+ return;
108
+ }
109
+ const pca = new PublicClientApplication({
110
+ auth: {
111
+ clientId: effectiveClientId,
112
+ authority: `https://login.microsoftonline.com/${effectiveTenantId}`,
113
+ redirectUri: effectiveRedirectUri
114
+ },
115
+ useBroker,
116
+ ...(useEmbeddedWebView ? {
117
+ androidConfigOptions: {
118
+ authorization_user_agent: 'WEBVIEW'
119
+ }
120
+ } : {})
121
+ });
122
+ pcaRef.current = pca;
123
+ const authInitScenario = getAppLogger().trackScenario(HostOperationNames.AuthInit, {
124
+ useBroker,
125
+ platform: Platform.OS
126
+ });
127
+ pca.init().then(() => pca.getAccounts()).then(accounts => {
128
+ const restoredAccount = accounts[0] ?? null;
129
+ setAccount(restoredAccount);
130
+ if (restoredAccount) {
131
+ getAppLogger().identifyUser({
132
+ objectId: restoredAccount.identifier,
133
+ tenantId: restoredAccount.tenantId
134
+ });
135
+ }
136
+ setIsAuthReady(true);
137
+ setIsLoading(false);
138
+ authInitScenario.complete({
139
+ accountFound: accounts.length > 0
140
+ });
141
+ }).catch(e => {
142
+ debugLog('[AuthProvider] MSAL init failed:', e);
143
+ setError(e instanceof Error ? e : new Error(String(e)));
144
+ setIsAuthReady(true);
145
+ setIsLoading(false);
146
+ authInitScenario.fail(undefined, {
147
+ errorName: getSafeErrorName(e)
148
+ });
149
+ });
150
+ // Re-initialize only when the effective broker/identity settings change.
151
+ // eslint-disable-next-line react-hooks/exhaustive-deps
152
+ }, [effectiveClientId, effectiveTenantId, effectiveRedirectUri, useBroker]);
153
+ const signIn = useCallback(async () => {
154
+ const pca = pcaRef.current;
155
+ if (!pca) return;
156
+ setIsLoading(true);
157
+ setError(null);
158
+ const signInScenario = getAppLogger().trackScenario(HostOperationNames.AuthSignIn, {
159
+ useBroker
160
+ });
161
+ try {
162
+ // If init() failed on mount, retry it now before attempting sign-in.
163
+ if (!pca.isInitialized) {
164
+ await pca.init();
165
+ }
166
+ const result = await pca.acquireToken({
167
+ scopes: SIGN_IN_SCOPES
168
+ });
169
+ setAccount(result.account);
170
+ getAppLogger().identifyUser({
171
+ objectId: result.account.identifier,
172
+ tenantId: result.account.tenantId
173
+ });
174
+ // Register the signed-in account with Intune MAM so the SDK can apply
175
+ // app protection policies (PIN, wipe, copy-paste restriction, etc.).
176
+ // Only triggered in release builds (intuneEnabled=true by default).
177
+ // Skipped for the dev client (intuneEnabled=false by default).
178
+ if (intuneEnabled) {
179
+ try {
180
+ await registerAndEnrollAccount({
181
+ upn: result.account.username,
182
+ aadId: result.account.identifier,
183
+ tenantId: result.account.tenantId
184
+ });
185
+ getAppLogger().trackActivityEvent(HostOperationNames.AuthIntuneEnroll);
186
+ } catch (intuneErr) {
187
+ // Log but don't block the user — enrollment failures surface via
188
+ // Intune Conditional Access on the next token acquisition.
189
+ debugWarn('[AuthProvider] Intune enrollment failed:', intuneErr);
190
+ getAppLogger().trackErrorEvent(HostOperationNames.AuthIntuneEnroll, {
191
+ errorName: getSafeErrorName(intuneErr)
192
+ });
193
+ }
194
+ }
195
+ signInScenario.complete();
196
+ } catch (e) {
197
+ setError(e instanceof Error ? e : new Error(String(e)));
198
+ signInScenario.fail(undefined, {
199
+ errorName: getSafeErrorName(e)
200
+ });
201
+ } finally {
202
+ setIsLoading(false);
203
+ }
204
+ }, [intuneEnabled, useBroker]);
205
+ const acquireToken = useCallback(async scopes => {
206
+ const pca = pcaRef.current;
207
+ if (!pca || !account) return null;
208
+ try {
209
+ const result = await pca.acquireTokenSilent({
210
+ scopes,
211
+ account
212
+ });
213
+ return result.accessToken;
214
+ } catch (e) {
215
+ // Silent acquisition failed — do NOT fall back to interactive here.
216
+ // Interactive sign-in is handled exclusively by signIn(). Falling back
217
+ // to interactive from acquireToken triggers an unexpected login webview
218
+ // whenever a connector needs a token the cache doesn't hold yet.
219
+ debugWarn('[AuthContext] acquireTokenSilent failed for scopes:', scopes, e);
220
+ getAppLogger().trackErrorEvent(HostOperationNames.AuthAcquireTokenSilentFailed, {
221
+ scopeCount: scopes.length,
222
+ errorName: getSafeErrorName(e)
223
+ });
224
+ return null;
225
+ }
226
+ }, [account]);
227
+ const signOut = useCallback(async () => {
228
+ const pca = pcaRef.current;
229
+ if (!pca || !account) return;
230
+ setIsLoading(true);
231
+ const signOutScenario = getAppLogger().trackScenario(HostOperationNames.AuthSignOut);
232
+ try {
233
+ if (intuneEnabled) {
234
+ try {
235
+ await deRegisterAndUnenrollAccount(account.username);
236
+ getAppLogger().trackActivityEvent(HostOperationNames.AuthIntuneUnenroll);
237
+ } catch (intuneErr) {
238
+ debugWarn('[AuthProvider] Intune unenroll failed:', intuneErr);
239
+ getAppLogger().trackErrorEvent(HostOperationNames.AuthIntuneUnenroll, {
240
+ errorName: getSafeErrorName(intuneErr)
241
+ });
242
+ }
243
+ }
244
+ await pca.signOut({
245
+ account
246
+ });
247
+ setAccount(null);
248
+ signOutScenario.complete();
249
+ } finally {
250
+ setIsLoading(false);
251
+ }
252
+ }, [account, intuneEnabled]);
253
+ return /*#__PURE__*/_jsx(AuthContext.Provider, {
254
+ value: {
255
+ isLoading,
256
+ isAuthReady,
257
+ isSignedIn: noAuthMode || account !== null,
258
+ hasRealAccount: account !== null,
259
+ error,
260
+ acquireToken,
261
+ signIn,
262
+ signOut
263
+ },
264
+ children: children
265
+ });
266
+ }
267
+
268
+ /**
269
+ * Returns the current auth state from the nearest <AuthProvider>.
270
+ * Must be called within the AuthProvider tree.
271
+ */
272
+ export function useAuth() {
273
+ const ctx = useContext(AuthContext);
274
+ if (!ctx) throw new Error('useAuth must be used within <AuthProvider>');
275
+ return ctx;
276
+ }
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+
3
+ /*!
4
+ * power-apps-native-host — NativePowerAppsBridge
5
+ *
6
+ * A PowerAppsBridge implementation for React Native that handles all plugin
7
+ * calls in-process: auth tokens are supplied by the host app via callbacks,
8
+ * and HTTP is performed with the global `fetch` (built into RN 0.60+).
9
+ *
10
+ * This file mirrors the bridge in the SDK but lives here so the package can
11
+ * be used without patching the published @microsoft/power-apps tarball.
12
+ */
13
+
14
+ import { debugLog, debugWarn } from "../logger.js";
15
+ import { getAppLogger, HostOperationNames } from "../telemetry/index.js";
16
+ import { sanitizeUrl } from '@microsoft/power-apps-native-common';
17
+ function getSafeErrorName(error) {
18
+ return error instanceof Error ? error.constructor.name : typeof error;
19
+ }
20
+
21
+ /**
22
+ * Per-connection-reference data keyed by data source name
23
+ * (e.g. "commondataserviceforapps", "office365users").
24
+ * Returned to the SDK for AppPowerAppsClientPlugin.loadAppConnectionsAsync_v2
25
+ * and AppPowerAppsClientPlugin.loadNonCompositeConnectionsAsync.
26
+ */
27
+
28
+ /** @deprecated Use AppConnectionReference */
29
+
30
+ /**
31
+ * Build a connection-references map (keyed by data source name) from the
32
+ * `connectionReferences` section of power.config.json.
33
+ *
34
+ * Each entry from power.config.json looks like:
35
+ * { id: "/providers/Microsoft.PowerApps/apis/shared_XXX", dataSources: ["XXX"], ... }
36
+ *
37
+ * The resulting map shape is what the SDK expects for loadAppConnectionsAsync_v2:
38
+ * { XXX: { apiId, runtimeUrl, connectionName, datasetName } }
39
+ *
40
+ * `runtimeUrl` and `connectionName` default to empty strings — they must be
41
+ * populated at runtime from the Power Platform connections API for the SDK to
42
+ * make actual connector API calls.
43
+ *
44
+ * @param powerConfigConnectionReferences - The `connectionReferences` object
45
+ * from power.config.json.
46
+ */
47
+ export function buildConnectionReferences(powerConfigConnectionReferences) {
48
+ const result = {};
49
+ for (const ref of Object.values(powerConfigConnectionReferences)) {
50
+ const entry = {
51
+ apiId: ref.id,
52
+ runtimeUrl: '',
53
+ connectionName: '',
54
+ datasetName: ''
55
+ };
56
+ for (const dsName of ref.dataSources) {
57
+ result[dsName] = entry;
58
+ }
59
+ }
60
+ return result;
61
+ }
62
+ /**
63
+ * The Power Apps web SDK annotates HTTP requests with internal auth schemes:
64
+ * - `paauth <token>` — connector/APIM calls
65
+ * - `dynamicauth <token>` — Dataverse OData calls
66
+ *
67
+ * Both are gateway-internal protocols. Native endpoints require the standard
68
+ * `Bearer` scheme, so rewrite either prefix before the request is sent.
69
+ */
70
+ function rewriteAuthHeaders(headers) {
71
+ if (!headers) return headers;
72
+ const auth = headers['Authorization'] ?? headers['authorization'];
73
+ if (auth?.startsWith('paauth ')) {
74
+ return {
75
+ ...headers,
76
+ Authorization: `Bearer ${auth.slice('paauth '.length)}`
77
+ };
78
+ }
79
+ if (auth?.startsWith('dynamicauth ')) {
80
+ return {
81
+ ...headers,
82
+ Authorization: `Bearer ${auth.slice('dynamicauth '.length)}`
83
+ };
84
+ }
85
+ return headers;
86
+ }
87
+ function normalizeJsonBodyHeaders(headers, body) {
88
+ const hasContentType = Object.keys(headers ?? {}).some(key => key.toLowerCase() === 'content-type');
89
+ const isJsonBody = typeof body === 'string' && /^[\s]*[\[{]/.test(body);
90
+ if (!isJsonBody || hasContentType) {
91
+ return headers;
92
+ }
93
+ return {
94
+ ...headers,
95
+ 'Content-Type': 'application/json'
96
+ };
97
+ }
98
+ function arrayBufferViewToArrayBuffer(view) {
99
+ const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
100
+ const copy = new Uint8Array(bytes.byteLength);
101
+ copy.set(bytes);
102
+ return copy.buffer;
103
+ }
104
+ export class NativePowerAppsBridge {
105
+ constructor(_options) {
106
+ this._options = _options;
107
+ }
108
+
109
+ /**
110
+ * Updates the bridge's options in-place.
111
+ *
112
+ * Call this when connection references or data sources change (e.g. after
113
+ * a new connection is created) without recreating the bridge. The SDK caches
114
+ * the bridge instance after the first data operation, so replacing the bridge
115
+ * via globalThis would have no effect — mutating options is the correct path.
116
+ */
117
+ updateOptions(options) {
118
+ this._options = options;
119
+ }
120
+ async initialize() {
121
+ // No handshake required for the native path.
122
+ }
123
+
124
+ /**
125
+ * Hermes-safe body construction. Returns the raw body value so that
126
+ * _sendHttpAsync can pass it directly to fetch without any Blob wrapping.
127
+ *
128
+ * The web-player's RuntimeDataClient wraps all bodies in `new Blob([...])`,
129
+ * which Hermes does not support for ArrayBuffer / TypedArray inputs.
130
+ * Implementing this hook on the bridge bypasses that wrapping entirely.
131
+ */
132
+ createRequestBody(config) {
133
+ const body = config.body;
134
+ if (ArrayBuffer.isView(body)) {
135
+ return arrayBufferViewToArrayBuffer(body);
136
+ }
137
+ return body ?? '';
138
+ }
139
+ async executePluginAsync(pluginName, pluginAction, params) {
140
+ const key = `${pluginName}.${pluginAction}`;
141
+ // Only log in dev-player — silenced in release APK builds.
142
+ debugLog(`[bridge] executePluginAsync: "${key}"`);
143
+ getAppLogger().trackVerboseEvent(HostOperationNames.BridgeUnhandledPlugin, {
144
+ pluginKey: key
145
+ });
146
+ switch (key) {
147
+ case 'AppIdentityServicePlugin.getAppAccessTokenAsync':
148
+ return this._options.getConnectorToken(params[0]);
149
+ case 'AppIdentityServicePlugin.getAppDynamicResourceAccessTokenAsync':
150
+ return this._options.getDataverseToken(params[0]);
151
+ case 'AppHttpClientPlugin.sendHttpAsync':
152
+ return this._sendHttpAsync(params);
153
+ case 'AppPowerAppsClientPlugin.getAppCdsDataSourceConfigsAsync':
154
+ return this._options.dataSources;
155
+ case 'AppPowerAppsClientPlugin.loadAppConnectionsAsync_v2':
156
+ return this._options.connections;
157
+
158
+ // Newer SDK versions split connection loading into two phases:
159
+ // loadNonCompositeConnectionsAsync (individual connections) and
160
+ // resolveCompositeConnectionsAsync (aggregated/composite connections).
161
+ // The return values are not used by the SDK caller (loadConnections())
162
+ // so we return the connections for the first phase and null for the second.
163
+ case 'AppPowerAppsClientPlugin.loadNonCompositeConnectionsAsync':
164
+ return this._options.connections;
165
+ case 'AppPowerAppsClientPlugin.resolveCompositeConnectionsAsync':
166
+ return null;
167
+
168
+ // Telemetry calls from the SDK are fire-and-forget — silently no-op them.
169
+ // The SDK reports these failures separately, which is noisy but safe.
170
+ case 'PublishedAppTelemetry.trackEvent':
171
+ case 'PublishedAppTelemetry.trackException':
172
+ case 'PublishedAppTelemetry.trackMetric':
173
+ case 'PublishedAppTelemetry.startScenario':
174
+ case 'PublishedAppTelemetry.endScenario':
175
+ case 'PublishedAppTelemetry.setDefaultProperties':
176
+ return undefined;
177
+ default:
178
+ // Warn instead of throw so unknown calls don't crash the app.
179
+ // Log the key only — params may contain auth tokens.
180
+ debugWarn(`[bridge] unhandled plugin call "${key}"`);
181
+ getAppLogger().trackErrorEvent(HostOperationNames.BridgeUnhandledPlugin, {
182
+ pluginKey: key
183
+ });
184
+ return undefined;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Calls fetch and returns the result in the exact shape expected by
190
+ * RuntimeDataClient._executeRequest:
191
+ * [{ status: number; headers: Record<string, string> }, ArrayBuffer]
192
+ */
193
+ async _sendHttpAsync(params) {
194
+ const requestConfig = params[0];
195
+ const bodyParam = params[1];
196
+ const {
197
+ url,
198
+ method
199
+ } = requestConfig;
200
+ const urlLabel = sanitizeUrl(url.replace(/^https?:\/\/[^/]+/, '').slice(0, 100));
201
+ const scenario = getAppLogger().trackScenario(HostOperationNames.BridgeHttpRequest, {
202
+ httpMethod: method,
203
+ url: urlLabel
204
+ });
205
+ const isBlob = bodyParam instanceof Blob;
206
+
207
+ // The SDK builds Authorization headers with the "paauth" scheme (its web-player
208
+ // convention). Native endpoints (APIM token exchange, connector runtime) expect
209
+ // standard "Bearer" tokens, so rewrite the scheme here before sending.
210
+ let headers = rewriteAuthHeaders(requestConfig.headers);
211
+ let bodyInit = null;
212
+ if (isBlob && bodyParam.size > 0) {
213
+ // The RN Blob polyfill does not implement arrayBuffer()/text() but RN's
214
+ // native fetch handles its own Blob type directly via internal blobId.
215
+ // Pass the Blob straight through — no conversion needed.
216
+ bodyInit = bodyParam;
217
+ } else if (typeof bodyParam === 'string' && bodyParam.length > 0) {
218
+ bodyInit = bodyParam;
219
+ } else if (bodyParam instanceof ArrayBuffer) {
220
+ bodyInit = bodyParam;
221
+ } else if (ArrayBuffer.isView(bodyParam)) {
222
+ // Binary buffer (e.g. file upload) — RN fetch is most reliable when the
223
+ // body is a standalone ArrayBuffer rather than a TypedArray view.
224
+ bodyInit = arrayBufferViewToArrayBuffer(bodyParam);
225
+ } else if (!isBlob && typeof bodyParam === 'object' && bodyParam !== null) {
226
+ // SDK passed a plain object — JSON-stringify it as a fallback
227
+ bodyInit = JSON.stringify(bodyParam);
228
+ }
229
+ headers = normalizeJsonBodyHeaders(headers, bodyInit);
230
+ debugLog(`[bridge] → ${method} ${urlLabel}`);
231
+ let response;
232
+ try {
233
+ response = await fetch(url, {
234
+ method,
235
+ headers: headers,
236
+ body: bodyInit
237
+ });
238
+ } catch (fetchErr) {
239
+ debugWarn(`[bridge] fetch THREW for ${method} ${urlLabel}:`, fetchErr);
240
+ debugLog(`[bridge] fetch THREW for ${method} ${urlLabel} | req body: ${typeof bodyInit === 'string' ? bodyInit.slice(0, 500) : bodyInit != null ? '<non-string body>' : '<no body>'}`);
241
+ scenario.fail(undefined, {
242
+ errorName: getSafeErrorName(fetchErr)
243
+ });
244
+ throw fetchErr;
245
+ }
246
+ const responseHeaders = {};
247
+ response.headers.forEach((value, key) => {
248
+ // fetch() normalises all header names to lowercase, but the SDK reads
249
+ // 'Content-Type' with title-case to decide how to parse the body.
250
+ // Map it back so the SDK doesn't see `undefined` and silently return no data.
251
+ const sdkKey = key === 'content-type' ? 'Content-Type' : key;
252
+ responseHeaders[sdkKey] = value;
253
+ });
254
+ const buffer = await response.arrayBuffer();
255
+ debugLog(`[bridge] ${method} ${urlLabel} → ${response.status}`);
256
+ if (!response.ok) {
257
+ const bodyText = new TextDecoder().decode(buffer).slice(0, 1000);
258
+ debugWarn(`[bridge] HTTP ${response.status} for ${method} ${urlLabel} | res: ${bodyText}`);
259
+ debugLog(`[bridge] HTTP ${response.status} for ${method} ${urlLabel} | req: ${typeof bodyInit === 'string' ? bodyInit.slice(0, 500) : bodyInit != null ? '<non-string body>' : '<no body>'} | res: ${bodyText}`);
260
+ scenario.completeWithError({
261
+ httpStatus: response.status
262
+ });
263
+ } else {
264
+ scenario.complete({
265
+ httpStatus: response.status
266
+ });
267
+ }
268
+ return [{
269
+ status: response.status,
270
+ headers: responseHeaders
271
+ }, buffer];
272
+ }
273
+ }