@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,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
|
+
}
|