@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,345 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* power-apps-native-host — PowerAppsProvider
|
|
5
|
+
*
|
|
6
|
+
* A single provider that composes all infrastructure required to run a
|
|
7
|
+
* Power Apps canvas app inside an Expo shell:
|
|
8
|
+
*
|
|
9
|
+
* GestureHandlerRootView
|
|
10
|
+
* └── TamaguiProvider (defaults to @tamagui/config/v3)
|
|
11
|
+
* └── PortalProvider (required by ToastViewport)
|
|
12
|
+
* └── ToastProvider + ToastViewport
|
|
13
|
+
* └── QueryClientProvider (own QueryClient, wired to NetInfo + AppState)
|
|
14
|
+
* └── AuthProvider
|
|
15
|
+
* └── PowerAppsHostProvider (connected via useAuth)
|
|
16
|
+
* └── children
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
*
|
|
20
|
+
* <PowerAppsProvider
|
|
21
|
+
* msalConfig={authConfig.msal}
|
|
22
|
+
* powerConfig={powerConfig}
|
|
23
|
+
* schemaMap={schemaMap}
|
|
24
|
+
* >
|
|
25
|
+
* <StatusBar style="auto" />
|
|
26
|
+
* <Slot />
|
|
27
|
+
* </PowerAppsProvider>
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
31
|
+
import { AppState, Platform } from 'react-native';
|
|
32
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
33
|
+
import { createTamagui, TamaguiProvider } from '@tamagui/core';
|
|
34
|
+
import { PortalProvider } from '@tamagui/portal';
|
|
35
|
+
import { config as tamaguiDefaultConfig } from '@tamagui/config/v3';
|
|
36
|
+
import { ToastProvider, ToastViewport } from '@tamagui/toast';
|
|
37
|
+
import { QueryClient, QueryClientProvider, focusManager, onlineManager } from '@tanstack/react-query';
|
|
38
|
+
import NetInfo from '@react-native-community/netinfo';
|
|
39
|
+
import { AuthProvider, useAuth } from "../auth/AuthContext.js";
|
|
40
|
+
import { PowerAppsHostProvider } from "./PowerAppsHostProvider.js";
|
|
41
|
+
import { fetchInstanceUrl } from "../platform/PowerPlatformEnvironmentApi.js";
|
|
42
|
+
import { ThemeProvider } from "../theme/ThemeProvider.js";
|
|
43
|
+
import { ErrorBoundary } from "../ErrorBoundary.js";
|
|
44
|
+
import Constants from 'expo-constants';
|
|
45
|
+
import { configureAppLogger, getAppLogger, Logger, OneDsTelemetryProvider, resolveDeviceId, resolveTelemetryLocation, regionToClusterCategory, suppressKnownWarnings } from '@microsoft/power-apps-native-common';
|
|
46
|
+
suppressKnownWarnings();
|
|
47
|
+
import { useOfflineIntegration } from '@microsoft/power-apps-native-offline';
|
|
48
|
+
|
|
49
|
+
// ---- React-Query wiring (runs once at module load) ----
|
|
50
|
+
|
|
51
|
+
// Wire onlineManager to NetInfo so refetchOnReconnect fires on mobile
|
|
52
|
+
// (the default browser-event-based detection is a no-op in React Native).
|
|
53
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
54
|
+
onlineManager.setEventListener(setOnline => {
|
|
55
|
+
return NetInfo.addEventListener(state => setOnline(!!state.isConnected));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Wire focusManager to AppState so queries refetch when the app foregrounds.
|
|
59
|
+
focusManager.setEventListener(handleFocus => {
|
|
60
|
+
const sub = AppState.addEventListener('change', status => {
|
|
61
|
+
if (Platform.OS !== 'web') handleFocus(status === 'active');
|
|
62
|
+
});
|
|
63
|
+
return () => sub.remove();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ---- Singletons ----
|
|
67
|
+
|
|
68
|
+
// The app's tamagui.config.ts (generated by /tamagui-design-system) is always
|
|
69
|
+
// passed as the `tamaguiConfig` prop and owns all $surface* / $accent* tokens.
|
|
70
|
+
// The bare @tamagui/config/v3 preset is only a last-resort fallback for tests
|
|
71
|
+
// and storybook — it will not resolve custom $surface* tokens.
|
|
72
|
+
const defaultTamaguiConfig = createTamagui(tamaguiDefaultConfig);
|
|
73
|
+
|
|
74
|
+
// Whether the offline SDK will actually orchestrate for this app. Mirrors the
|
|
75
|
+
// resolution that @microsoft/power-apps-native-offline's useOfflineIntegration
|
|
76
|
+
// performs (offlineProfile prop → expo manifest `extra.__offlineProfile`;
|
|
77
|
+
// enabled defaults true unless the profile's `appConfig.enabled` is explicitly
|
|
78
|
+
// false), but computed synchronously here because the QueryClient's networkMode
|
|
79
|
+
// must be fixed BEFORE the first query runs. The SDK's own `isEnabled` is only
|
|
80
|
+
// known deep inside the tree (below QueryClientProvider), which is too late to
|
|
81
|
+
// configure the client without a race. Kept deliberately narrow: it only
|
|
82
|
+
// decides networkMode below.
|
|
83
|
+
function isOfflineEnabled(offlineProfile) {
|
|
84
|
+
// Only honour the prop when it carries a profileId — this mirrors the SDK's
|
|
85
|
+
// useOfflineIntegration, which ignores a profileId-less prop and falls through
|
|
86
|
+
// to the manifest. Without this guard a truthy-but-invalid prop (no profileId)
|
|
87
|
+
// short-circuits to `false` here while the SDK still resolves enabled from the
|
|
88
|
+
// manifest, leaving networkMode on 'online' (blank offline lists).
|
|
89
|
+
const propProfile = offlineProfile;
|
|
90
|
+
let profile = propProfile && propProfile.profileId ? propProfile : undefined;
|
|
91
|
+
if (!profile) {
|
|
92
|
+
const fromManifest = Constants.expoConfig?.extra?.__offlineProfile;
|
|
93
|
+
if (fromManifest && typeof fromManifest === 'object' && fromManifest.profileId) {
|
|
94
|
+
profile = fromManifest;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (profile) {
|
|
98
|
+
// Honour an explicit `appConfig.enabled: false`. This mirrors the SDK's
|
|
99
|
+
// useOfflineIntegration, whose validateConfig classifies `enabled === false`
|
|
100
|
+
// as "explicitly-disabled" and stays inert. A host shell's placeholder
|
|
101
|
+
// profile (e.g. Power Apps Dev Player's offline-profile.json, enabled:false)
|
|
102
|
+
// must NOT flip networkMode to 'always' for the loaded app — otherwise every
|
|
103
|
+
// scanned app gets paused queries / blank lists. Any other resolved profile
|
|
104
|
+
// (enabled true, or omitted → SDK defaults to true) enables offline.
|
|
105
|
+
const appConfig = profile.appConfig;
|
|
106
|
+
if (appConfig && appConfig.enabled === false) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Builds the app's QueryClient. networkMode is gated on whether offline is
|
|
115
|
+
// actually enabled:
|
|
116
|
+
// • Offline enabled → 'always': the host is offline-first, so reads/writes are
|
|
117
|
+
// served by the offline SDK from local SQLite regardless of connectivity.
|
|
118
|
+
// onlineManager is wired to NetInfo (above) for refetchOnReconnect, but that
|
|
119
|
+
// same wiring makes the default 'online' mode PAUSE every query/mutation while
|
|
120
|
+
// offline (queryFn never runs → blank lists with WiFi off). 'always' makes
|
|
121
|
+
// React Query ignore connectivity for both the initial attempt AND retries, so
|
|
122
|
+
// local-SQLite reads/writes never get paused. ('offlineFirst' would still pause
|
|
123
|
+
// *retries* offline — wrong for a purely-local SDK where a transient first
|
|
124
|
+
// attempt error, e.g. SDK-not-ready at cold boot, would leave the query stuck
|
|
125
|
+
// 'paused' until reconnect.)
|
|
126
|
+
// • Offline disabled → undefined: React Query keeps its default 'online' mode,
|
|
127
|
+
// i.e. the exact pre-offline behaviour for online-only apps.
|
|
128
|
+
function createQueryClient(offlineEnabled) {
|
|
129
|
+
const networkMode = offlineEnabled ? 'always' : undefined;
|
|
130
|
+
return new QueryClient({
|
|
131
|
+
defaultOptions: {
|
|
132
|
+
queries: {
|
|
133
|
+
staleTime: 60_000,
|
|
134
|
+
retry: 2,
|
|
135
|
+
networkMode
|
|
136
|
+
},
|
|
137
|
+
mutations: {
|
|
138
|
+
retry: 0,
|
|
139
|
+
networkMode
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---- Types ----
|
|
146
|
+
|
|
147
|
+
/** Shape of power.config.json consumed by the provider. */
|
|
148
|
+
|
|
149
|
+
// ---- Inner component ----
|
|
150
|
+
// Separate from PowerAppsProvider only because useAuth() must be called inside
|
|
151
|
+
// AuthProvider. All auth-dependent wiring lives here.
|
|
152
|
+
|
|
153
|
+
function InnerProviders({
|
|
154
|
+
msalConfig,
|
|
155
|
+
powerConfig,
|
|
156
|
+
schemaMap,
|
|
157
|
+
renderOAuthWebView,
|
|
158
|
+
onDisambiguationNeeded,
|
|
159
|
+
offlineProfile,
|
|
160
|
+
children
|
|
161
|
+
}) {
|
|
162
|
+
const {
|
|
163
|
+
acquireToken,
|
|
164
|
+
isSignedIn,
|
|
165
|
+
hasRealAccount
|
|
166
|
+
} = useAuth();
|
|
167
|
+
const connectionReferences = useMemo(() => powerConfig.connectionReferences ?? {}, [powerConfig.connectionReferences]);
|
|
168
|
+
|
|
169
|
+
// ---- Offline (all logic lives in the SDK) ----
|
|
170
|
+
// Adapter over the host's fetchInstanceUrl: the offline SDK's contract passes a
|
|
171
|
+
// 4th `signal?: AbortSignal` arg (which fetchInstanceUrl treats as bearerToken)
|
|
172
|
+
// and expects a `string | null` return, so drop the extra arg and normalise
|
|
173
|
+
// `undefined` -> `null`.
|
|
174
|
+
const resolveInstanceUrlForOffline = useCallback(async (environmentId, region, acqToken, _signal) => {
|
|
175
|
+
const url = await fetchInstanceUrl(environmentId, region, acqToken);
|
|
176
|
+
return url ?? null;
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
// Gate offline engagement on a REAL account (`hasRealAccount`), not the
|
|
180
|
+
// `isSignedIn` flag — `isSignedIn` is inflated by noAuthMode (login-bypassed /
|
|
181
|
+
// demo sessions report signed-in but have no account, so `acquireToken` always
|
|
182
|
+
// returns null). Passing the inflated flag makes the SDK orchestrator engage
|
|
183
|
+
// and then spin in a perpetual "waiting-for-auth" 3s retry loop (each retry
|
|
184
|
+
// re-renders the provider subtree). With no obtainable token offline cannot
|
|
185
|
+
// sync anyway, so we only engage once a usable token exists.
|
|
186
|
+
const offline = useOfflineIntegration({
|
|
187
|
+
powerConfig,
|
|
188
|
+
offlineProfile,
|
|
189
|
+
acquireToken,
|
|
190
|
+
isSignedIn: hasRealAccount,
|
|
191
|
+
fetchInstanceUrl: resolveInstanceUrlForOffline
|
|
192
|
+
});
|
|
193
|
+
return /*#__PURE__*/_jsxs(PowerAppsHostProvider, {
|
|
194
|
+
acquireToken: acquireToken,
|
|
195
|
+
isSignedIn: isSignedIn,
|
|
196
|
+
appId: powerConfig.appId ?? undefined,
|
|
197
|
+
environmentId: powerConfig.environmentId ?? '',
|
|
198
|
+
region: powerConfig.region ?? 'prod',
|
|
199
|
+
connectionReferences: connectionReferences,
|
|
200
|
+
schemaMap: schemaMap,
|
|
201
|
+
databaseReferences: powerConfig.databaseReferences,
|
|
202
|
+
isvClientId: msalConfig.clientId,
|
|
203
|
+
renderOAuthWebView: renderOAuthWebView,
|
|
204
|
+
onDisambiguationNeeded: onDisambiguationNeeded,
|
|
205
|
+
children: [children, offline.StatusOverlay && /*#__PURE__*/_jsx(offline.StatusOverlay, {})]
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---- Public component ----
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Top-level provider for Power Apps canvas apps running inside an Expo shell.
|
|
213
|
+
*
|
|
214
|
+
* Composes GestureHandlerRootView, TamaguiProvider, ToastProvider,
|
|
215
|
+
* QueryClientProvider, AuthProvider, and PowerAppsHostProvider into a single
|
|
216
|
+
* component, eliminating all provider boilerplate from the consuming app's
|
|
217
|
+
* root layout.
|
|
218
|
+
*/
|
|
219
|
+
export function PowerAppsProvider({
|
|
220
|
+
msalConfig,
|
|
221
|
+
intuneConfig,
|
|
222
|
+
powerConfig,
|
|
223
|
+
schemaMap,
|
|
224
|
+
tamaguiConfig = defaultTamaguiConfig,
|
|
225
|
+
// override with app's tamagui.config.ts
|
|
226
|
+
defaultTheme = 'light',
|
|
227
|
+
theme,
|
|
228
|
+
darkTheme,
|
|
229
|
+
followSystemTheme = true,
|
|
230
|
+
renderOAuthWebView,
|
|
231
|
+
onDisambiguationNeeded,
|
|
232
|
+
appName,
|
|
233
|
+
logger: loggerProp,
|
|
234
|
+
offlineProfile,
|
|
235
|
+
children
|
|
236
|
+
}) {
|
|
237
|
+
// Capture the logger-prop identity at mount time so the effect dep array
|
|
238
|
+
// is stable (loggerProp is an object reference; re-running on every render
|
|
239
|
+
// would reconfigure and leak the previous provider).
|
|
240
|
+
const loggerPropRef = useRef(loggerProp);
|
|
241
|
+
|
|
242
|
+
// Gate the offline-first networkMode on whether offline is actually enabled
|
|
243
|
+
// for this app. Keyed on the resolved boolean (not the prop object refs) so the
|
|
244
|
+
// QueryClient identity stays stable across renders even if the caller passes
|
|
245
|
+
// fresh offlineProfile literals.
|
|
246
|
+
const offlineEnabled = useMemo(() => isOfflineEnabled(offlineProfile), [offlineProfile]);
|
|
247
|
+
const queryClient = useMemo(() => createQueryClient(offlineEnabled), [offlineEnabled]);
|
|
248
|
+
|
|
249
|
+
// Configure the app logger exactly once per mount inside useEffect so that
|
|
250
|
+
// side effects (provider construction, async resolveDeviceId) never run
|
|
251
|
+
// during the render phase. This is safe because AppLogger.ts starts with a
|
|
252
|
+
// silent no-op Logger, so any telemetry emitted before the first commit is
|
|
253
|
+
// silently dropped rather than crashing.
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
const lp = loggerPropRef.current;
|
|
256
|
+
if (lp === null) {
|
|
257
|
+
// Explicit opt-out — replace with a silent no-op Logger.
|
|
258
|
+
configureAppLogger(new Logger());
|
|
259
|
+
} else if (lp !== undefined) {
|
|
260
|
+
// Caller supplied their own fully-configured logger.
|
|
261
|
+
configureAppLogger(lp);
|
|
262
|
+
} else {
|
|
263
|
+
// Default: auto-wire the 1DS JS adapter so telemetry flows out of the
|
|
264
|
+
// box without any boilerplate in the consuming app.
|
|
265
|
+
const providerConfig = {
|
|
266
|
+
clusterCategory: regionToClusterCategory(powerConfig.region ?? 'dev'),
|
|
267
|
+
clientType: appName ?? 'PowerAppsNative',
|
|
268
|
+
appVersion: Constants.expoConfig?.version,
|
|
269
|
+
minSeverity: __DEV__ ? 'verbose' : 'info',
|
|
270
|
+
telemetryLocation: resolveTelemetryLocation(powerConfig.geoRegion)
|
|
271
|
+
};
|
|
272
|
+
// Resolve the stable device ID first so every event — including the
|
|
273
|
+
// very first — carries the real GUID rather than the 'react-native'
|
|
274
|
+
// fallback. Events emitted while this resolves are silently dropped
|
|
275
|
+
// by the no-op Logger that is active until configureAppLogger is called.
|
|
276
|
+
resolveDeviceId().then(deviceId => {
|
|
277
|
+
const provider = new OneDsTelemetryProvider({
|
|
278
|
+
...providerConfig,
|
|
279
|
+
deviceId
|
|
280
|
+
});
|
|
281
|
+
configureAppLogger(new Logger({
|
|
282
|
+
providers: [provider]
|
|
283
|
+
}));
|
|
284
|
+
}).catch(_err => {
|
|
285
|
+
// SecureStore unavailable — fall back to a provider without a stable ID.
|
|
286
|
+
const provider = new OneDsTelemetryProvider(providerConfig);
|
|
287
|
+
configureAppLogger(new Logger({
|
|
288
|
+
providers: [provider]
|
|
289
|
+
}));
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Flush any queued telemetry when this component unmounts (dev reload /
|
|
294
|
+
// process exit). Placed inside this effect so the cleanup is paired with
|
|
295
|
+
// the setup — if the effect re-runs (which it won't here, empty deps),
|
|
296
|
+
// the previous provider is flushed before the new one is configured.
|
|
297
|
+
return () => {
|
|
298
|
+
void getAppLogger().flush();
|
|
299
|
+
};
|
|
300
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
301
|
+
}, []); // intentionally empty — logger is configured once per mount
|
|
302
|
+
return /*#__PURE__*/_jsx(ErrorBoundary, {
|
|
303
|
+
children: /*#__PURE__*/_jsx(GestureHandlerRootView, {
|
|
304
|
+
style: {
|
|
305
|
+
flex: 1
|
|
306
|
+
},
|
|
307
|
+
children: /*#__PURE__*/_jsx(ThemeProvider, {
|
|
308
|
+
theme: theme,
|
|
309
|
+
darkTheme: darkTheme,
|
|
310
|
+
followSystem: followSystemTheme,
|
|
311
|
+
children: /*#__PURE__*/_jsx(TamaguiProvider, {
|
|
312
|
+
config: tamaguiConfig,
|
|
313
|
+
defaultTheme: defaultTheme,
|
|
314
|
+
children: /*#__PURE__*/_jsx(PortalProvider, {
|
|
315
|
+
shouldAddRootHost: true,
|
|
316
|
+
children: /*#__PURE__*/_jsxs(ToastProvider, {
|
|
317
|
+
swipeDirection: "horizontal",
|
|
318
|
+
duration: 3000,
|
|
319
|
+
children: [/*#__PURE__*/_jsx(QueryClientProvider, {
|
|
320
|
+
client: queryClient,
|
|
321
|
+
children: /*#__PURE__*/_jsx(AuthProvider, {
|
|
322
|
+
msalConfig: msalConfig,
|
|
323
|
+
intuneConfig: intuneConfig,
|
|
324
|
+
children: /*#__PURE__*/_jsx(InnerProviders, {
|
|
325
|
+
msalConfig: msalConfig,
|
|
326
|
+
powerConfig: powerConfig,
|
|
327
|
+
schemaMap: schemaMap,
|
|
328
|
+
renderOAuthWebView: renderOAuthWebView,
|
|
329
|
+
onDisambiguationNeeded: onDisambiguationNeeded,
|
|
330
|
+
offlineProfile: offlineProfile,
|
|
331
|
+
children: children
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
}), /*#__PURE__*/_jsx(ToastViewport, {
|
|
335
|
+
top: 50,
|
|
336
|
+
left: 0,
|
|
337
|
+
right: 0
|
|
338
|
+
})]
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
});
|
|
345
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
5
|
+
*
|
|
6
|
+
* power-apps-native-host — package telemetry
|
|
7
|
+
*
|
|
8
|
+
* The shared logger singleton lives in `@microsoft/power-apps-native-common`
|
|
9
|
+
* (`getAppLogger` / `configureAppLogger`). This module re-exports those
|
|
10
|
+
* alongside the canonical host operation-name constants.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { getAppLogger, configureAppLogger } from '@microsoft/power-apps-native-common';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Canonical operation names for host package telemetry.
|
|
17
|
+
* Follow the `PowerAppsHost.<Subsystem>.<Action>` convention so events are
|
|
18
|
+
* filterable in Kusto by prefix.
|
|
19
|
+
*/
|
|
20
|
+
export const HostOperationNames = {
|
|
21
|
+
// ── Auth ─────────────────────────────────────────────────────────────────
|
|
22
|
+
/** MSAL PublicClientApplication.init() + initial account resolution. */
|
|
23
|
+
AuthInit: 'PowerAppsHost.Auth.Init',
|
|
24
|
+
/** Interactive sign-in (acquireToken interactive). */
|
|
25
|
+
AuthSignIn: 'PowerAppsHost.Auth.SignIn',
|
|
26
|
+
/** Sign-out (token cache clear + broker logout). */
|
|
27
|
+
AuthSignOut: 'PowerAppsHost.Auth.SignOut',
|
|
28
|
+
/** Silent token acquisition. Recorded only on failure. */
|
|
29
|
+
AuthAcquireTokenSilentFailed: 'PowerAppsHost.Auth.AcquireTokenSilent.Failed',
|
|
30
|
+
/** Intune MAM enrollment after sign-in. */
|
|
31
|
+
AuthIntuneEnroll: 'PowerAppsHost.Auth.Intune.Enroll',
|
|
32
|
+
/** Intune MAM unenrollment during sign-out. */
|
|
33
|
+
AuthIntuneUnenroll: 'PowerAppsHost.Auth.Intune.Unenroll',
|
|
34
|
+
// ── Bridge / HTTP ─────────────────────────────────────────────────────────
|
|
35
|
+
/** SDK-originated HTTP request relayed through NativePowerAppsBridge. */
|
|
36
|
+
BridgeHttpRequest: 'PowerAppsHost.Bridge.HttpRequest',
|
|
37
|
+
/** Plugin call forwarded through NativePowerAppsBridge (verbose; also used for unhandled). */
|
|
38
|
+
BridgeUnhandledPlugin: 'PowerAppsHost.Bridge.PluginCall',
|
|
39
|
+
/** Connector/Dataverse token acquisition for the bridge. Recorded only on failure. */
|
|
40
|
+
BridgeTokenAcquireFailed: 'PowerAppsHost.Bridge.TokenAcquire.Failed',
|
|
41
|
+
// ── Runtime ───────────────────────────────────────────────────────────────
|
|
42
|
+
/** Top-level React error boundary caught an unhandled JS error. */
|
|
43
|
+
RuntimeUnhandledJsError: 'PowerAppsHost.Runtime.UnhandledJsError',
|
|
44
|
+
// ── Platform APIs ─────────────────────────────────────────────────────────
|
|
45
|
+
/** Resolve Dataverse instanceUrl from Power Platform environment API. */
|
|
46
|
+
PlatformFetchInstanceUrl: 'PowerAppsHost.Platform.FetchInstanceUrl',
|
|
47
|
+
/** useConnectionRefs failed to resolve connections (recoverable). Recorded only on failure. */
|
|
48
|
+
PlatformLoadConnectionsFailed: 'PowerAppsHost.Platform.LoadConnections.Failed',
|
|
49
|
+
/** Create a new Power Platform connection. */
|
|
50
|
+
PlatformCreateConnection: 'PowerAppsHost.Platform.CreateConnection',
|
|
51
|
+
/** Resolve app connection references for the current launch. */
|
|
52
|
+
PlatformResolveConnectionRefs: 'PowerAppsHost.Platform.ResolveConnectionRefs',
|
|
53
|
+
/** Grant this app access to a Power Platform connection ACL. */
|
|
54
|
+
PlatformConnectionAclGrant: 'PowerAppsHost.Platform.ConnectionAclGrant'
|
|
55
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ThemeProvider — distributes {@link ThemeTokens} through the component tree.
|
|
5
|
+
*
|
|
6
|
+
* `PowerAppsProvider` mounts this automatically. Use directly only when
|
|
7
|
+
* composing providers manually.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { createContext, useContext, useMemo, useState } from 'react';
|
|
11
|
+
import { useColorScheme } from 'react-native';
|
|
12
|
+
import { lightTheme as defaultLightTheme, darkTheme as defaultDarkTheme } from "./tokens.js";
|
|
13
|
+
|
|
14
|
+
// ── Contexts ─────────────────────────────────────────────────────────────────
|
|
15
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
16
|
+
const ThemeContext = /*#__PURE__*/createContext(defaultLightTheme);
|
|
17
|
+
const ThemeControlContext = /*#__PURE__*/createContext({
|
|
18
|
+
setTheme: () => {},
|
|
19
|
+
resetTheme: () => {}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ── Provider ─────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export function ThemeProvider({
|
|
25
|
+
theme = defaultLightTheme,
|
|
26
|
+
darkTheme = defaultDarkTheme,
|
|
27
|
+
followSystem = true,
|
|
28
|
+
children
|
|
29
|
+
}) {
|
|
30
|
+
const colorScheme = useColorScheme();
|
|
31
|
+
const [override, setOverride] = useState(null);
|
|
32
|
+
const resolved = useMemo(() => {
|
|
33
|
+
if (override !== null) return override;
|
|
34
|
+
return followSystem && colorScheme === 'dark' ? darkTheme : theme;
|
|
35
|
+
}, [theme, darkTheme, followSystem, colorScheme, override]);
|
|
36
|
+
const control = useMemo(() => ({
|
|
37
|
+
setTheme: tokens => setOverride(tokens),
|
|
38
|
+
resetTheme: () => setOverride(null)
|
|
39
|
+
}), []);
|
|
40
|
+
return /*#__PURE__*/_jsx(ThemeControlContext.Provider, {
|
|
41
|
+
value: control,
|
|
42
|
+
children: /*#__PURE__*/_jsx(ThemeContext.Provider, {
|
|
43
|
+
value: resolved,
|
|
44
|
+
children: children
|
|
45
|
+
})
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Hooks ─────────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/** Returns current {@link ThemeTokens} from the nearest ThemeProvider. */
|
|
52
|
+
export function useThemeTokens() {
|
|
53
|
+
return useContext(ThemeContext);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns `{ setTheme, resetTheme }` for imperative theme overrides.
|
|
58
|
+
* Stable reference — does not re-render consumers on theme change.
|
|
59
|
+
*/
|
|
60
|
+
export function useThemeControl() {
|
|
61
|
+
return useContext(ThemeControlContext);
|
|
62
|
+
}
|
|
63
|
+
export { lightTheme, darkTheme } from "./tokens.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Theme token schema and built-in palettes for power-apps-native-host.
|
|
5
|
+
*
|
|
6
|
+
* Pass a custom {@link ThemeTokens} object to `PowerAppsProvider` to override
|
|
7
|
+
* the default Microsoft blue palette.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ── Built-in themes ──────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/** Microsoft blue light palette (default). */
|
|
13
|
+
export const lightTheme = {
|
|
14
|
+
surface0: '#ffffff',
|
|
15
|
+
surface1: '#f7f7f8',
|
|
16
|
+
surface2: '#efeff1',
|
|
17
|
+
surface3: '#e3e3e7',
|
|
18
|
+
surface4: '#d0d0d6',
|
|
19
|
+
text0: '#111113',
|
|
20
|
+
text1: '#2e2e33',
|
|
21
|
+
text2: '#696969',
|
|
22
|
+
text3: '#9e9e9e',
|
|
23
|
+
accentDeep: '#004578',
|
|
24
|
+
accentBase: '#0078d4',
|
|
25
|
+
accentSoft: '#deecf9',
|
|
26
|
+
accentOnAccent: '#ffffff',
|
|
27
|
+
isDark: false
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Microsoft blue dark palette (default). */
|
|
31
|
+
export const darkTheme = {
|
|
32
|
+
surface0: '#111113',
|
|
33
|
+
surface1: '#1c1c1f',
|
|
34
|
+
surface2: '#28282c',
|
|
35
|
+
surface3: '#333338',
|
|
36
|
+
surface4: '#404047',
|
|
37
|
+
text0: '#ededef',
|
|
38
|
+
text1: '#c8c8cc',
|
|
39
|
+
text2: '#9898a0',
|
|
40
|
+
text3: '#6b6b75',
|
|
41
|
+
accentDeep: '#82c7ff',
|
|
42
|
+
accentBase: '#2899f5',
|
|
43
|
+
accentSoft: '#0a2238',
|
|
44
|
+
accentOnAccent: '#ffffff',
|
|
45
|
+
isDark: true
|
|
46
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Utilities for reading local file:// URIs and converting them into the binary
|
|
5
|
+
* format expected by the Dataverse upload endpoint via NativePowerAppsBridge.
|
|
6
|
+
*
|
|
7
|
+
* Background:
|
|
8
|
+
* - `fetch('file://')` fails silently on Android.
|
|
9
|
+
* - `fetch('data:...')` throws "Network request failed" on Android.
|
|
10
|
+
* - `new Blob([Uint8Array])` crashes Hermes ("ArrayBufferView not supported").
|
|
11
|
+
* - NativePowerAppsBridge accepts `Uint8Array` and extracts `.buffer` as the
|
|
12
|
+
* fetch body, which React Native sends as raw bytes correctly.
|
|
13
|
+
*
|
|
14
|
+
* Solution: read via `readAsStringAsync` (base64) → `atob` → `Uint8Array`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readAsStringAsync, EncodingType } from 'expo-file-system/legacy';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read a local `file://` URI and return its contents as a `Uint8Array`.
|
|
21
|
+
* Safe on both Android and iOS.
|
|
22
|
+
*/
|
|
23
|
+
export async function readFileAsUint8Array(uri) {
|
|
24
|
+
const base64 = await readAsStringAsync(uri, {
|
|
25
|
+
encoding: EncodingType.Base64
|
|
26
|
+
});
|
|
27
|
+
return base64ToUint8Array(base64);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detect the image MIME type from the first bytes of a base64-encoded string.
|
|
32
|
+
* Returns 'image/jpeg' as a safe default.
|
|
33
|
+
*/
|
|
34
|
+
export function detectImageMime(base64) {
|
|
35
|
+
const normalized = normalizeBase64(base64);
|
|
36
|
+
if (normalized.startsWith('iVBOR')) return 'image/png';
|
|
37
|
+
if (normalized.startsWith('R0lG')) return 'image/gif';
|
|
38
|
+
if (normalized.startsWith('Qk')) return 'image/bmp';
|
|
39
|
+
if (normalized.startsWith('UklGR')) return 'image/webp';
|
|
40
|
+
return 'image/jpeg';
|
|
41
|
+
}
|
|
42
|
+
function normalizeBase64(base64) {
|
|
43
|
+
return base64.includes('base64,') ? base64.slice(base64.indexOf('base64,') + 'base64,'.length) : base64;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Convert a base64 string into bytes suitable for Dataverse binary upload APIs. */
|
|
47
|
+
export function base64ToUint8Array(base64) {
|
|
48
|
+
const normalized = normalizeBase64(base64);
|
|
49
|
+
const binaryString = atob(normalized);
|
|
50
|
+
const uint8 = new Uint8Array(binaryString.length);
|
|
51
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
52
|
+
uint8[i] = binaryString.charCodeAt(i);
|
|
53
|
+
}
|
|
54
|
+
return uint8;
|
|
55
|
+
}
|
|
56
|
+
function uint8ArrayToArrayBuffer(bytes) {
|
|
57
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
58
|
+
copy.set(bytes);
|
|
59
|
+
return copy.buffer;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a lightweight File-compatible object for generated Dataverse upload methods.
|
|
64
|
+
* React Native does not consistently provide a browser File implementation, but the
|
|
65
|
+
* generated services only require `name` and `arrayBuffer()`.
|
|
66
|
+
*/
|
|
67
|
+
export function createDataverseUploadFile(name, mimeType, bytes) {
|
|
68
|
+
return {
|
|
69
|
+
name,
|
|
70
|
+
type: mimeType,
|
|
71
|
+
size: bytes.byteLength,
|
|
72
|
+
arrayBuffer: async () => uint8ArrayToArrayBuffer(bytes)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Create a File-compatible image object from Dataverse-style base64 image content. */
|
|
77
|
+
export function base64ToDataverseUploadFile(base64, name = 'image') {
|
|
78
|
+
const bytes = base64ToUint8Array(base64);
|
|
79
|
+
const mimeType = detectImageMime(base64);
|
|
80
|
+
const extension = mimeType.slice('image/'.length).replace('jpeg', 'jpg');
|
|
81
|
+
return createDataverseUploadFile(name.includes('.') ? name : `${name}.${extension}`, mimeType, bytes);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build a `data:<mime>;base64,<data>` URI from a raw base64 string returned
|
|
86
|
+
* by Dataverse for an image column.
|
|
87
|
+
*/
|
|
88
|
+
export function base64ToDataUri(base64) {
|
|
89
|
+
return `data:${detectImageMime(base64)};base64,${base64}`;
|
|
90
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { NativeModules } from 'react-native';
|
|
4
|
+
import Constants from 'expo-constants';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* True when the JS bundle is running inside the Power Apps Dev Player shell.
|
|
8
|
+
*
|
|
9
|
+
* Two signals are combined to cover both execution paths:
|
|
10
|
+
*
|
|
11
|
+
* 1. `Constants.expoConfig.extra.isDevPlayer` — set in apps/dev-player/app.config.js.
|
|
12
|
+
* Fires when the dev-player's **own** JS bundle is active (e.g. the launcher screen).
|
|
13
|
+
*
|
|
14
|
+
* 2. `NativeModules.PowerAppsDevLauncher` — the dev-player's Expo native module,
|
|
15
|
+
* registered in the dev-player APK/IPA regardless of which JS bundle is loaded.
|
|
16
|
+
* Fires when a template bundle is loaded via QR-code/URL into the dev-player shell,
|
|
17
|
+
* because expo-constants then reflects the template's manifest (no isDevPlayer flag)
|
|
18
|
+
* but the native module is still present in the host binary.
|
|
19
|
+
*
|
|
20
|
+
* Neither signal fires in a standalone release APK generated from the template, so
|
|
21
|
+
* `isDevPlayer` is false there and all debug logging is silenced.
|
|
22
|
+
*/
|
|
23
|
+
export const isDevPlayer = Constants.expoConfig?.extra?.isDevPlayer === true || !!NativeModules.PowerAppsDevLauncher;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface Props {
|
|
3
|
+
children: React.ReactNode;
|
|
4
|
+
}
|
|
5
|
+
interface State {
|
|
6
|
+
hasError: boolean;
|
|
7
|
+
error: Error | null;
|
|
8
|
+
componentStack: string | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Top-level React error boundary.
|
|
12
|
+
*
|
|
13
|
+
* Catches any unhandled JS exception thrown during rendering / lifecycle and
|
|
14
|
+
* displays a diagnostic screen. All error details are written to the native
|
|
15
|
+
* log via debugLog so diagnostics stay consistent with the host package logger.
|
|
16
|
+
*
|
|
17
|
+
* Place this component at the very root of the component tree so it captures
|
|
18
|
+
* failures in any screen.
|
|
19
|
+
*
|
|
20
|
+
* Exported from power-apps-native-host so it can be used in the template and
|
|
21
|
+
* in any custom app without duplicating the implementation.
|
|
22
|
+
*/
|
|
23
|
+
export declare class ErrorBoundary extends React.Component<Props, State> {
|
|
24
|
+
state: State;
|
|
25
|
+
static getDerivedStateFromError(error: Error): Partial<State>;
|
|
26
|
+
componentDidCatch(error: Error, info: React.ErrorInfo): void;
|
|
27
|
+
render(): React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=ErrorBoundary.d.ts.map
|