@microsoft/power-apps-native-host 0.2.22 → 0.3.0

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 (156) hide show
  1. package/README.md +162 -26
  2. package/compatibility/current.json +87 -0
  3. package/lib/NOTICE.txt +140 -1226
  4. package/lib/commonjs/auth/AuthContext.js +74 -18
  5. package/lib/commonjs/auth/AuthContext.web.js +32 -0
  6. package/lib/commonjs/auth/completePowerAppsAuthSession.js +16 -0
  7. package/lib/commonjs/bridge/NativePowerAppsBridge.js +20 -17
  8. package/lib/commonjs/bridge/PowerPagesBridge.js +157 -0
  9. package/lib/commonjs/bridge/bridgeState.js +40 -0
  10. package/lib/commonjs/connections/ConnectionSetupScreen.js +459 -80
  11. package/lib/commonjs/connections/PowerPlatformConnectionApi.js +946 -0
  12. package/lib/commonjs/connections/connectionMetadata.js +127 -0
  13. package/lib/commonjs/connections/connectionState.js +96 -0
  14. package/lib/commonjs/connections/disambiguate.js +19 -11
  15. package/lib/commonjs/connections/resolveConnections.js +347 -0
  16. package/lib/commonjs/connections/types.js +106 -0
  17. package/lib/commonjs/{platform → connections}/useConnectionRefs.js +128 -65
  18. package/lib/commonjs/connections/useConnectionSetup.js +111 -52
  19. package/lib/commonjs/controls/FilePicker.js +6 -0
  20. package/lib/commonjs/controls/ImagePicker.js +8 -0
  21. package/lib/commonjs/index.js +188 -6
  22. package/lib/commonjs/logger.js +9 -6
  23. package/lib/commonjs/offline/createOfflineHostActivationCallbacks.js +61 -0
  24. package/lib/commonjs/offline/useOfflineIntegration.js +12 -0
  25. package/lib/commonjs/offline/useOfflineIntegration.web.js +11 -0
  26. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.js +46 -0
  27. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.web.js +13 -0
  28. package/lib/commonjs/platform/DataverseDataSources.js +14 -0
  29. package/lib/commonjs/platform/DataverseGlobalDiscoveryApi.js +49 -0
  30. package/lib/commonjs/platform/GlobalDiscovery.js +51 -0
  31. package/lib/commonjs/platform/PowerPlatformEnvironmentApi.js +6 -106
  32. package/lib/commonjs/provider/PowerAppsHostProvider.js +156 -25
  33. package/lib/commonjs/provider/PowerAppsHostProvider.web.js +41 -0
  34. package/lib/commonjs/provider/PowerAppsProvider.js +229 -133
  35. package/lib/commonjs/provider/SessionDetailsGesture.js +216 -0
  36. package/lib/commonjs/telemetry/OfflineSdkTelemetry.js +95 -0
  37. package/lib/commonjs/telemetry/OfflineSdkTelemetry.web.js +9 -0
  38. package/lib/commonjs/telemetry/index.js +22 -3
  39. package/lib/module/auth/AuthContext.js +74 -18
  40. package/lib/module/auth/AuthContext.web.js +26 -0
  41. package/lib/module/auth/completePowerAppsAuthSession.js +11 -0
  42. package/lib/module/bridge/NativePowerAppsBridge.js +21 -18
  43. package/lib/module/bridge/PowerPagesBridge.js +151 -0
  44. package/lib/module/bridge/bridgeState.js +34 -0
  45. package/lib/module/connections/ConnectionSetupScreen.js +460 -81
  46. package/lib/module/connections/PowerPlatformConnectionApi.js +932 -0
  47. package/lib/module/connections/connectionMetadata.js +112 -0
  48. package/lib/module/connections/connectionState.js +82 -0
  49. package/lib/module/connections/disambiguate.js +19 -12
  50. package/lib/module/connections/resolveConnections.js +342 -0
  51. package/lib/module/connections/types.js +91 -0
  52. package/lib/module/{platform → connections}/useConnectionRefs.js +129 -66
  53. package/lib/module/connections/useConnectionSetup.js +113 -54
  54. package/lib/module/controls/FilePicker.js +6 -0
  55. package/lib/module/controls/ImagePicker.js +8 -0
  56. package/lib/module/index.js +14 -6
  57. package/lib/module/logger.js +8 -6
  58. package/lib/module/offline/createOfflineHostActivationCallbacks.js +55 -0
  59. package/lib/module/offline/useOfflineIntegration.js +3 -0
  60. package/lib/module/offline/useOfflineIntegration.web.js +7 -0
  61. package/lib/module/offline/useOfflineIntegrationWithStartupGate.js +42 -0
  62. package/lib/module/offline/useOfflineIntegrationWithStartupGate.web.js +9 -0
  63. package/lib/module/platform/DataverseDataSources.js +10 -0
  64. package/lib/module/platform/DataverseGlobalDiscoveryApi.js +45 -0
  65. package/lib/module/platform/GlobalDiscovery.js +45 -0
  66. package/lib/module/platform/PowerPlatformEnvironmentApi.js +5 -104
  67. package/lib/module/provider/PowerAppsHostProvider.js +159 -28
  68. package/lib/module/provider/PowerAppsHostProvider.web.js +36 -0
  69. package/lib/module/provider/PowerAppsProvider.js +233 -137
  70. package/lib/module/provider/SessionDetailsGesture.js +211 -0
  71. package/lib/module/telemetry/OfflineSdkTelemetry.js +91 -0
  72. package/lib/module/telemetry/OfflineSdkTelemetry.web.js +5 -0
  73. package/lib/module/telemetry/index.js +15 -4
  74. package/lib/typescript/commonjs/auth/AuthContext.d.ts +3 -1
  75. package/lib/typescript/commonjs/auth/AuthContext.web.d.ts +21 -0
  76. package/lib/typescript/commonjs/auth/completePowerAppsAuthSession.d.ts +8 -0
  77. package/lib/typescript/commonjs/bridge/PowerPagesBridge.d.ts +18 -0
  78. package/lib/typescript/commonjs/bridge/bridgeState.d.ts +5 -0
  79. package/lib/typescript/commonjs/connections/ConnectionSetupScreen.d.ts +28 -7
  80. package/lib/typescript/commonjs/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  81. package/lib/typescript/commonjs/connections/connectionMetadata.d.ts +66 -0
  82. package/lib/typescript/commonjs/connections/connectionState.d.ts +16 -0
  83. package/lib/typescript/commonjs/connections/disambiguate.d.ts +4 -2
  84. package/lib/typescript/commonjs/{platform → connections}/resolveConnections.d.ts +19 -19
  85. package/lib/typescript/commonjs/connections/types.d.ts +129 -1
  86. package/lib/typescript/commonjs/{platform → connections}/useConnectionRefs.d.ts +33 -24
  87. package/lib/typescript/commonjs/connections/useConnectionSetup.d.ts +12 -8
  88. package/lib/typescript/commonjs/controls/FilePicker.d.ts +7 -1
  89. package/lib/typescript/commonjs/controls/ImagePicker.d.ts +9 -1
  90. package/lib/typescript/commonjs/index.d.ts +20 -10
  91. package/lib/typescript/commonjs/logger.d.ts +4 -4
  92. package/lib/typescript/commonjs/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  93. package/lib/typescript/commonjs/offline/useOfflineIntegration.d.ts +2 -0
  94. package/lib/typescript/commonjs/offline/useOfflineIntegration.web.d.ts +4 -0
  95. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  96. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  97. package/lib/typescript/commonjs/platform/DataverseDataSources.d.ts +27 -0
  98. package/lib/typescript/commonjs/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  99. package/lib/typescript/commonjs/platform/GlobalDiscovery.d.ts +4 -0
  100. package/lib/typescript/commonjs/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  101. package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +29 -18
  102. package/lib/typescript/commonjs/provider/PowerAppsHostProvider.web.d.ts +5 -0
  103. package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +32 -10
  104. package/lib/typescript/commonjs/provider/SessionDetailsGesture.d.ts +13 -0
  105. package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.d.ts +15 -0
  106. package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
  107. package/lib/typescript/commonjs/telemetry/index.d.ts +13 -1
  108. package/lib/typescript/module/auth/AuthContext.d.ts +3 -1
  109. package/lib/typescript/module/auth/AuthContext.web.d.ts +21 -0
  110. package/lib/typescript/module/auth/completePowerAppsAuthSession.d.ts +8 -0
  111. package/lib/typescript/module/bridge/PowerPagesBridge.d.ts +18 -0
  112. package/lib/typescript/module/bridge/bridgeState.d.ts +5 -0
  113. package/lib/typescript/module/connections/ConnectionSetupScreen.d.ts +28 -7
  114. package/lib/typescript/module/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  115. package/lib/typescript/module/connections/connectionMetadata.d.ts +66 -0
  116. package/lib/typescript/module/connections/connectionState.d.ts +16 -0
  117. package/lib/typescript/module/connections/disambiguate.d.ts +4 -2
  118. package/lib/typescript/module/{platform → connections}/resolveConnections.d.ts +19 -19
  119. package/lib/typescript/module/connections/types.d.ts +129 -1
  120. package/lib/typescript/module/{platform → connections}/useConnectionRefs.d.ts +33 -24
  121. package/lib/typescript/module/connections/useConnectionSetup.d.ts +12 -8
  122. package/lib/typescript/module/controls/FilePicker.d.ts +7 -1
  123. package/lib/typescript/module/controls/ImagePicker.d.ts +9 -1
  124. package/lib/typescript/module/index.d.ts +24 -14
  125. package/lib/typescript/module/logger.d.ts +4 -4
  126. package/lib/typescript/module/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  127. package/lib/typescript/module/offline/useOfflineIntegration.d.ts +2 -0
  128. package/lib/typescript/module/offline/useOfflineIntegration.web.d.ts +4 -0
  129. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  130. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  131. package/lib/typescript/module/platform/DataverseDataSources.d.ts +27 -0
  132. package/lib/typescript/module/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  133. package/lib/typescript/module/platform/GlobalDiscovery.d.ts +4 -0
  134. package/lib/typescript/module/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  135. package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +29 -18
  136. package/lib/typescript/module/provider/PowerAppsHostProvider.web.d.ts +5 -0
  137. package/lib/typescript/module/provider/PowerAppsProvider.d.ts +34 -12
  138. package/lib/typescript/module/provider/SessionDetailsGesture.d.ts +13 -0
  139. package/lib/typescript/module/telemetry/OfflineSdkTelemetry.d.ts +15 -0
  140. package/lib/typescript/module/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
  141. package/lib/typescript/module/telemetry/index.d.ts +13 -1
  142. package/package.json +43 -18
  143. package/scripts/build-ios-base.js +66 -27
  144. package/scripts/bundle.js +212 -5
  145. package/scripts/generate-connector-schemas.js +8 -6
  146. package/scripts/metro-logger.js +213 -0
  147. package/scripts/start-web.mjs +137 -0
  148. package/scripts/upgrade/runtime.js +463 -0
  149. package/scripts/upgrade/system.js +244 -0
  150. package/scripts/upgrade-authoring.js +534 -0
  151. package/scripts/upgrade.js +188 -0
  152. package/scripts/wrap.js +536 -0
  153. package/lib/commonjs/platform/PowerPlatformConnectionApi.js +0 -606
  154. package/lib/commonjs/platform/resolveConnections.js +0 -264
  155. package/lib/module/platform/PowerPlatformConnectionApi.js +0 -595
  156. package/lib/module/platform/resolveConnections.js +0 -259
@@ -0,0 +1,536 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/build.js — Production build for the Power Apps standalone template.
4
+ *
5
+ * Relies entirely on the Expo build pipeline — no bundletool, no signing code,
6
+ * no post-build binary patching. All customer values (bundle ID, display name,
7
+ * ADAL credentials, URL schemes) are injected via environment variables before
8
+ * `expo prebuild` runs, so the native project is fully configured before the
9
+ * native toolchain compiles it.
10
+ *
11
+ * Android flow:
12
+ * 1. Write auth.config.json (MSAL clientId / tenantId)
13
+ * 2. expo prebuild --platform android (env vars bake in bundle ID + ADAL)
14
+ * 3. ./gradlew assembleRelease (produces an unsigned release APK)
15
+ * 4. Copy APK → dist/<bundleId>.apk
16
+ *
17
+ * iOS flow (unsigned — default, for the wrap pipeline):
18
+ * 1. Write auth.config.json
19
+ * 2. expo prebuild --platform ios (env vars bake in bundle ID + ADAL + URL schemes)
20
+ * 3. pod install
21
+ * 4. build-ios-base (xcodebuild archive → unsigned IPA)
22
+ * 5. Copy IPA → dist/<bundleId>.ipa
23
+ *
24
+ * iOS flow (signed — when ios.signing is set in wrap.config.json):
25
+ * 1-4. Same as above
26
+ * 5. xcodebuild archive (with DEVELOPMENT_TEAM for automatic signing)
27
+ * 6. xcodebuild -exportArchive (produces signed IPA via ExportOptions.plist)
28
+ *
29
+ * Usage:
30
+ * node scripts/build.js android — produces dist/<bundleId>.apk (signed)
31
+ * node scripts/build.js ios — produces dist/<bundleId>.ipa (signed or unsigned)
32
+ *
33
+ * Prerequisites — Android:
34
+ * - JDK 17+: https://adoptium.net/ | brew install openjdk@17
35
+ * - ANDROID_HOME pointing to the Android SDK
36
+ * - Android SDK Platform-Tools + Build-Tools on PATH
37
+ *
38
+ * Prerequisites — iOS (macOS only):
39
+ * - Xcode + Command Line Tools (xcode-select --install)
40
+ * - CocoaPods (brew install cocoapods)
41
+ *
42
+ * The output IPA (iOS) is unsigned when ios.signing is absent (suitable for the
43
+ * wrap pipeline), or signed via xcodebuild automatic signing when ios.signing is
44
+ * configured. The Android APK is signed by apksigner using the
45
+ * keystore configured in wrap.config.json (debug keystore for local testing).
46
+ *
47
+ * Configuration: fill in wrap.config.json (copy from wrap.config.json.example).
48
+ */
49
+ 'use strict';
50
+
51
+ const { execFileSync } = require('child_process');
52
+ const path = require('path');
53
+ const fs = require('fs');
54
+ const os = require('os');
55
+
56
+ // ─── Parse args ───────────────────────────────────────────────────────────────
57
+ const projectRoot = process.cwd();
58
+ const platform = (process.argv[2] || '').toLowerCase();
59
+
60
+ if (platform !== 'android' && platform !== 'ios') {
61
+ console.error('Usage: wrap android|ios');
62
+ process.exit(1);
63
+ }
64
+
65
+ // ─── Load wrap.config.json ────────────────────────────────────────────────────
66
+ const configPath = path.join(projectRoot, 'wrap.config.json');
67
+ if (!fs.existsSync(configPath)) {
68
+ console.error(
69
+ '✗ wrap.config.json not found.\n' +
70
+ ' Copy wrap.config.json.example, fill in your values, then re-run.'
71
+ );
72
+ process.exit(1);
73
+ }
74
+
75
+ let config;
76
+ try {
77
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
78
+ } catch (e) {
79
+ console.error(`✗ wrap.config.json is not valid JSON: ${e.message}`);
80
+ process.exit(1);
81
+ }
82
+
83
+ for (const field of ['bundleIdentifier', 'displayName']) {
84
+ if (!config[field]) {
85
+ console.error(`✗ wrap.config.json: missing required field "${field}"`);
86
+ process.exit(1);
87
+ }
88
+ }
89
+ if (!config.msal?.clientId || !config.msal?.tenantId) {
90
+ console.error('✗ wrap.config.json: missing required fields msal.clientId / msal.tenantId');
91
+ process.exit(1);
92
+ }
93
+
94
+ const {
95
+ bundleIdentifier,
96
+ displayName,
97
+ version = '1.0.0',
98
+ versionCode = 1,
99
+ msal: { clientId, tenantId },
100
+ outputPath: outputPathRel = './dist',
101
+ } = config;
102
+
103
+ const outputDir = path.resolve(projectRoot, outputPathRel);
104
+
105
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
106
+ function step(n, total, msg) {
107
+ console.log(`\n${'─'.repeat(60)}`);
108
+ console.log(` Step ${n}/${total} — ${msg}`);
109
+ console.log('─'.repeat(60));
110
+ }
111
+
112
+ function ok(msg) { console.log(` ✓ ${msg}`); }
113
+ function warn(msg) { console.log(` ⚠ ${msg}`); }
114
+
115
+ function run(cmd, args, opts = {}) {
116
+ const label = [cmd, ...args].join(' ');
117
+ console.log(`\n▶ ${label.length > 120 ? label.slice(0, 117) + '...' : label}`);
118
+ execFileSync(cmd, args, { stdio: 'inherit', ...opts });
119
+ }
120
+
121
+ function requireCommand(cmd, hint) {
122
+ try {
123
+ // Use `where` on Windows, `which` on Unix/macOS.
124
+ const checker = process.platform === 'win32' ? 'where' : 'which';
125
+ execFileSync(checker, [cmd], { stdio: 'ignore' });
126
+ } catch {
127
+ console.error(`✗ "${cmd}" not found. ${hint}`);
128
+ process.exit(1);
129
+ }
130
+ }
131
+
132
+ // ─── Step: write auth.config.json ─────────────────────────────────────────────
133
+ function updateAuthConfig() {
134
+ const authPath = path.join(projectRoot, 'auth.config.json');
135
+ fs.writeFileSync(authPath, JSON.stringify({ msal: { clientId, tenantId } }, null, 2) + '\n');
136
+ ok(`auth.config.json updated (clientId: ${clientId})`);
137
+ }
138
+
139
+ // ─── Step: expo prebuild ──────────────────────────────────────────────────────
140
+ // All customer values are passed as env vars. app.config.js reads them and
141
+ // injects them into the native project so no post-build binary patching is needed.
142
+ function runPrebuild(plat) {
143
+ const appSlug = bundleIdentifier.replace(/\./g, '-').toLowerCase();
144
+
145
+ const env = {
146
+ ...process.env,
147
+ // Identity
148
+ ANDROID_PACKAGE: bundleIdentifier,
149
+ IOS_BUNDLE_IDENTIFIER: bundleIdentifier,
150
+ APP_DISPLAY_NAME: displayName,
151
+ APP_SLUG: appSlug,
152
+ APP_SCHEME: appSlug,
153
+ // Version — applied to the Expo config's version and Android versionCode / iOS buildNumber.
154
+ APP_VERSION: version,
155
+ APP_VERSION_CODE: String(versionCode),
156
+ // ADAL / MAM — picked up by app.config.js → baked into Info.plist (iOS)
157
+ // and Android string resources (via the power-apps-native-host config plugin).
158
+ ADAL_CLIENT_ID: clientId,
159
+ ADAL_TENANT_ID: tenantId,
160
+ // iOS auth-broker redirect URI — msauth.<bundleId>://auth
161
+ IOS_ADAL_REDIRECT_URI: `msauth.${bundleIdentifier}://auth`,
162
+ // App icon — 1024×1024 PNG; Expo generates all platform sizes at prebuild time.
163
+ ...(config.iconPath ? { APP_ICON_PATH: path.resolve(projectRoot, config.iconPath) } : {}),
164
+ };
165
+
166
+ run('npx', ['expo', 'prebuild', '--platform', plat, '--clean'], { env });
167
+ ok(`expo prebuild (${plat}) complete`);
168
+ }
169
+
170
+ // ─── Compute base64-encoded SHA-1 keystore signature hash ────────────────────
171
+ // The MSAL Android redirect URI is msauth://<bundleId>/<base64-SHA1>. The hash
172
+ // comes from the signing certificate, so this must be called with the same
173
+ // keystore that signed the APK.
174
+ function computeSignatureHash(ksPath, ksPassword, alias) {
175
+ const out = execFileSync('keytool', [
176
+ '-list', '-keystore', ksPath,
177
+ '-storepass', ksPassword,
178
+ '-alias', alias,
179
+ '-v',
180
+ ]).toString();
181
+ const match = out.match(/SHA1:\s+([0-9A-Fa-f:]{59})/i);
182
+ if (!match) throw new Error('Could not extract SHA1 from keytool output');
183
+ return Buffer.from(match[1].replace(/:/g, ''), 'hex').toString('base64');
184
+ }
185
+
186
+ // ─── Pre-flight: ensure Intune MAM assets are present ─────────────────────────────
187
+ // Downloads on-demand assets required before the native build. The auth
188
+ // package ships without the large binary artifacts; this ensures they exist.
189
+ function ensureMamAssets(plat) {
190
+ const authPkg = path.dirname(require.resolve('@microsoft/power-apps-native-auth/package.json'));
191
+ const { sentinel, setupScript, label } = plat === 'android'
192
+ ? {
193
+ sentinel: path.join(authPkg, 'android', 'libs', 'com.microsoft.intune.mam.build.jar'),
194
+ setupScript: path.join(authPkg, 'scripts', 'setup-android-sdk.js'),
195
+ label: 'MAM SDK jars',
196
+ }
197
+ : {
198
+ sentinel: path.join(authPkg, 'ios', 'Frameworks', 'IntuneMAMSwift.xcframework'),
199
+ setupScript: path.join(authPkg, 'scripts', 'setup-ios-frameworks.js'),
200
+ label: 'MAM iOS xcframeworks',
201
+ };
202
+
203
+ if (fs.existsSync(sentinel)) return;
204
+ console.log(`\n Intune ${label} not found — downloading now...`);
205
+ if (!fs.existsSync(setupScript)) {
206
+ console.error(`✗ Setup script not found: ${setupScript}`);
207
+ console.error(' Reinstall @microsoft/power-apps-native-auth and retry.');
208
+ process.exit(1);
209
+ }
210
+ run(process.execPath, [setupScript]);
211
+ if (!fs.existsSync(sentinel)) {
212
+ console.error(`✗ Setup completed but ${label} still missing — check network and retry.`);
213
+ process.exit(1);
214
+ }
215
+ ok(`Intune ${label} ready`);
216
+ }
217
+
218
+ // ─── Decode a .mobileprovision file to a plain JS object ──────────────────────────
219
+ // Strips the DER-wrapped CMS envelope with security(1) then converts the
220
+ // resulting plist to JSON with plutil(1).
221
+ function decodeProvisioningProfile(profPath) {
222
+ requireCommand('security', 'xcode-select --install');
223
+ requireCommand('plutil', 'xcode-select --install');
224
+ const tmpPlist = path.join(os.tmpdir(), `pawrap-prof-${process.pid}.plist`);
225
+ fs.writeFileSync(
226
+ tmpPlist,
227
+ execFileSync('security', ['cms', '-D', '-i', profPath], { stdio: ['ignore', 'pipe', 'ignore'] }),
228
+ );
229
+ try {
230
+ return JSON.parse(
231
+ execFileSync('plutil', ['-convert', 'json', '-o', '-', tmpPlist],
232
+ { stdio: ['ignore', 'pipe', 'ignore'] }).toString(),
233
+ );
234
+ } finally {
235
+ fs.rmSync(tmpPlist, { force: true });
236
+ }
237
+ }
238
+
239
+ // ─── Write ExportOptions.plist for xcodebuild -exportArchive ──────────────────
240
+ // Returns the path to the written file.
241
+ function writeExportOptionsPlist({ exportMethod, signingStyle, teamId, profileName }) {
242
+ const entries = [
243
+ `\t<key>method</key>\n\t<string>${exportMethod}</string>`,
244
+ `\t<key>signingStyle</key>\n\t<string>${signingStyle}</string>`,
245
+ teamId ? `\t<key>teamID</key>\n\t<string>${teamId}</string>` : '',
246
+ profileName ? `\t<key>provisioningProfiles</key>\n\t<dict>\n\t\t<key>${bundleIdentifier}</key>\n\t\t<string>${profileName}</string>\n\t</dict>` : '',
247
+ ].filter(Boolean).join('\n');
248
+ const plistPath = path.join(os.tmpdir(), 'pawrap-ExportOptions.plist');
249
+ fs.writeFileSync(plistPath, [
250
+ '<?xml version="1.0" encoding="UTF-8"?>',
251
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
252
+ '<plist version="1.0">',
253
+ '<dict>',
254
+ entries,
255
+ '</dict>',
256
+ '</plist>',
257
+ ].join('\n'));
258
+ return plistPath;
259
+ }
260
+
261
+ function buildAndroid() {
262
+ requireCommand('keytool', 'Included with the JDK (https://adoptium.net/)');
263
+ requireCommand('apksigner', 'Add $ANDROID_HOME/build-tools/<version> to PATH');
264
+ requireCommand('unzip', 'xcode-select --install (macOS) | apt install unzip (Linux)');
265
+
266
+ ensureMamAssets('android');
267
+
268
+ const { android: androidCfg = {} } = config;
269
+ const {
270
+ keystorePath = '',
271
+ keystorePassword = 'android',
272
+ keyAlias = 'androiddebugkey',
273
+ keyPassword = 'android',
274
+ } = androidCfg;
275
+
276
+ // Resolve keystore — fall back to the auto-generated Expo debug keystore.
277
+ // The debug keystore works for local testing but must NOT be used for
278
+ // Play Store submissions (use a dedicated release keystore instead).
279
+ let ksPath = keystorePath
280
+ ? path.resolve(projectRoot, keystorePath)
281
+ : path.join(projectRoot, 'android', 'app', 'debug.keystore');
282
+ const usingDebugKs = !keystorePath;
283
+
284
+ const TOTAL = 5;
285
+
286
+ step(1, TOTAL, 'Update auth.config.json');
287
+ updateAuthConfig();
288
+
289
+ step(2, TOTAL, 'expo prebuild (android)');
290
+ runPrebuild('android');
291
+
292
+ // After prebuild the debug keystore exists at android/app/debug.keystore.
293
+ // Resolve the path again so the fallback is valid post-prebuild.
294
+ if (usingDebugKs) ksPath = path.join(projectRoot, 'android', 'app', 'debug.keystore');
295
+
296
+ if (!fs.existsSync(ksPath)) {
297
+ console.error(`✗ Keystore not found: ${ksPath}`);
298
+ console.error(' Set android.keystorePath in wrap.config.json.');
299
+ console.error(' Generate: keytool -genkey -v -keystore release.jks -alias release -keyalg RSA -keysize 2048 -validity 10000');
300
+ process.exit(1);
301
+ }
302
+ if (usingDebugKs) warn('No android.keystorePath set — signing with the Expo debug keystore (testing only).');
303
+
304
+ step(3, TOTAL, 'Build release APK (gradlew assembleRelease)');
305
+ run('./gradlew', ['assembleRelease'], { cwd: path.join(projectRoot, 'android') });
306
+
307
+ // The ShrinkWrap product flavor (injected by the auth config plugin) changes
308
+ // the Gradle output path from outputs/apk/release/ to outputs/apk/ShrinkWrap/release/.
309
+ // Search one level of flavor subdirectory to handle both flavored and plain builds.
310
+ const apkBaseDir = path.join(projectRoot, 'android', 'app', 'build', 'outputs', 'apk');
311
+ let apks = [];
312
+ if (fs.existsSync(apkBaseDir)) {
313
+ // Try the flavored path first (ShrinkWrap/release/), then fall back to release/
314
+ for (const entry of fs.readdirSync(apkBaseDir)) {
315
+ const releaseDir = path.join(apkBaseDir, entry, 'release');
316
+ if (fs.existsSync(releaseDir)) {
317
+ const found = fs.readdirSync(releaseDir).filter(f => f.endsWith('.apk'));
318
+ apks.push(...found.map(f => path.join(releaseDir, f)));
319
+ }
320
+ }
321
+ }
322
+ if (!apks.length) {
323
+ console.error(`✗ No APK found under ${apkBaseDir}`);
324
+ process.exit(1);
325
+ }
326
+ // assembleRelease produces an unsigned APK when no signingConfig is set.
327
+ // Prefer the *-unsigned.apk variant for determinism; fall back to the
328
+ // first match sorted alphabetically.
329
+ apks.sort();
330
+ const unsignedApk = apks.find(f => path.basename(f).includes('-unsigned')) ?? apks[0];
331
+
332
+ step(4, TOTAL, 'Sign APK (apksigner)');
333
+ // The MSAL redirect URI is derived from the signing certificate, so we must
334
+ // sign before computing the redirect URI — unsigned APKs cannot authenticate.
335
+ fs.mkdirSync(outputDir, { recursive: true });
336
+ const signedApk = path.join(outputDir, `${bundleIdentifier}.apk`);
337
+ run('apksigner', [
338
+ 'sign',
339
+ '--ks', ksPath,
340
+ '--ks-pass', `pass:${keystorePassword}`,
341
+ '--ks-key-alias', keyAlias,
342
+ '--key-pass', `pass:${keyPassword}`,
343
+ '--out', signedApk,
344
+ unsignedApk,
345
+ ]);
346
+ ok(`Signed APK: ${signedApk}`);
347
+
348
+ step(5, TOTAL, 'Compute MSAL redirect URI');
349
+ try {
350
+ const sigHash = computeSignatureHash(ksPath, keystorePassword, keyAlias);
351
+ const redirectUri = `msauth://${bundleIdentifier}/${sigHash}`;
352
+ console.log('\n ── Add to your Entra app registration ──────────────');
353
+ console.log(` Redirect URI (Android / Public client):`)
354
+ console.log(` ${redirectUri}`);
355
+ if (usingDebugKs) {
356
+ warn('This URI uses the debug keystore — register a release keystore URI for Play Store.');
357
+ }
358
+ console.log(' ─────────────────────────────────────────────────────');
359
+ } catch (e) {
360
+ warn(`Could not compute redirect URI: ${e.message}`);
361
+ }
362
+ }
363
+
364
+
365
+ // ═════════════════════════════════════════════════════════════════════════════
366
+ // iOS
367
+ //
368
+ // Two build paths depending on whether ios.signing is configured:
369
+ //
370
+ // Unsigned (no ios.signing): build-ios-base → unsigned IPA for the wrap
371
+ // pipeline. Suitable when you want to produce a base binary that will be
372
+ // re-signed downstream (e.g. by the PowerApps shrinkwrap service).
373
+ //
374
+ // Signed (ios.signing present): xcodebuild archive + xcodebuild -exportArchive
375
+ // xcodebuild’s native automatic signing handles certificate and provisioning-
376
+ // profile selection (including downloading from the Developer Portal) — no
377
+ // custom profile-search logic needed.
378
+ //
379
+ // ios.signing fields:
380
+ // exportMethod — "development" | "ad-hoc" | "app-store" | "enterprise"
381
+ // teamId — Apple Developer team ID (e.g. "ABCD1234EF").
382
+ // Recommended; required if multiple teams are in Xcode.
383
+ // provisioningProfile — optional path to a .mobileprovision file.
384
+ // When omitted, xcodebuild uses automatic signing and
385
+ // downloads / selects the profile from the Developer Portal.
386
+ // When provided, the profile is installed locally and
387
+ // signingStyle=manual is used.
388
+ // ═════════════════════════════════════════════════════════════════════════════
389
+ function buildIos() {
390
+ if (process.platform !== 'darwin') {
391
+ console.error('✗ iOS builds require macOS.');
392
+ process.exit(1);
393
+ }
394
+ requireCommand('pod', 'brew install cocoapods');
395
+
396
+ const { ios: iosCfg = {} } = config;
397
+ const signing = iosCfg.signing;
398
+ const TOTAL = 6;
399
+
400
+ step(1, TOTAL, 'Update auth.config.json');
401
+ updateAuthConfig();
402
+
403
+ step(2, TOTAL, 'Download Intune MAM iOS xcframeworks');
404
+ ensureMamAssets('ios');
405
+
406
+ step(3, TOTAL, 'expo prebuild (ios)');
407
+ runPrebuild('ios');
408
+
409
+ step(4, TOTAL, 'pod install');
410
+ run('pod', ['install'], { cwd: path.join(projectRoot, 'ios') });
411
+
412
+ if (signing) {
413
+ // ── Signed path: xcodebuild archive + exportArchive ─────────────────────────────
414
+ // xcodebuild’s native signing handles certificate + profile selection automatically.
415
+ requireCommand('xcodebuild', 'xcode-select --install');
416
+
417
+ const {
418
+ exportMethod = 'development',
419
+ teamId = '',
420
+ provisioningProfile: profileRel = '',
421
+ } = signing;
422
+
423
+ const iosDir = path.join(projectRoot, 'ios');
424
+ const xcworkspace = fs.readdirSync(iosDir).find(f => f.endsWith('.xcworkspace'));
425
+ if (!xcworkspace) throw new Error('No .xcworkspace found in ios/ — run pod install first.');
426
+ const scheme = xcworkspace.replace('.xcworkspace', '');
427
+ const archivePath = path.join(os.tmpdir(), 'pawrap-signed.xcarchive');
428
+
429
+ step(5, TOTAL, 'Archive (xcodebuild archive)');
430
+ const archiveArgs = [
431
+ '-workspace', path.join(iosDir, xcworkspace),
432
+ '-scheme', scheme,
433
+ '-configuration', 'Release',
434
+ 'archive',
435
+ '-archivePath', archivePath,
436
+ '-destination', 'generic/platform=iOS',
437
+ ];
438
+ if (teamId) archiveArgs.push(`DEVELOPMENT_TEAM=${teamId}`);
439
+ run('xcodebuild', archiveArgs);
440
+
441
+ step(6, TOTAL, 'Export signed IPA (xcodebuild -exportArchive)');
442
+
443
+ // If an explicit profile file is provided, install it to the local profiles
444
+ // directory by UUID so xcodebuild can reference it by name.
445
+ let signingStyle = 'automatic';
446
+ let profileName = '';
447
+ if (profileRel) {
448
+ const profPath = path.resolve(projectRoot, profileRel);
449
+ if (!fs.existsSync(profPath)) {
450
+ console.error(`✗ Provisioning profile not found: ${profPath}`);
451
+ process.exit(1);
452
+ }
453
+ const profMeta = decodeProvisioningProfile(profPath);
454
+ const destDir = path.join(os.homedir(), 'Library', 'MobileDevice', 'Provisioning Profiles');
455
+ fs.mkdirSync(destDir, { recursive: true });
456
+ fs.copyFileSync(profPath, path.join(destDir, `${profMeta.UUID}.mobileprovision`));
457
+ profileName = profMeta.Name;
458
+ signingStyle = 'manual';
459
+ ok(`Installed provisioning profile: ${profileName}`);
460
+ }
461
+
462
+ const exportOptsPlist = writeExportOptionsPlist({ exportMethod, signingStyle, teamId, profileName });
463
+
464
+ const exportDir = path.join(os.tmpdir(), 'pawrap-export');
465
+ fs.rmSync(exportDir, { recursive: true, force: true });
466
+ run('xcodebuild', [
467
+ '-exportArchive',
468
+ '-archivePath', archivePath,
469
+ '-exportOptionsPlist', exportOptsPlist,
470
+ '-exportPath', exportDir,
471
+ ]);
472
+
473
+ const ipas = fs.readdirSync(exportDir).filter(f => f.endsWith('.ipa'));
474
+ if (!ipas.length) throw new Error(`No IPA found in export directory: ${exportDir}`);
475
+ fs.mkdirSync(outputDir, { recursive: true });
476
+ const dest = path.join(outputDir, `${bundleIdentifier}.ipa`);
477
+ fs.copyFileSync(path.join(exportDir, ipas[0]), dest);
478
+ ok(`Signed IPA: ${dest}`);
479
+
480
+ } else {
481
+ // ── Unsigned path: build-ios-base ───────────────────────────────────────────────
482
+ // build-ios-base runs xcodebuild with CODE_SIGNING_REQUIRED=NO and packages
483
+ // the result into /tmp/pawrap-base.ipa — ready for the wrap pipeline to re-sign.
484
+ requireCommand('zip', 'xcode-select --install');
485
+ requireCommand('unzip', 'xcode-select --install');
486
+
487
+ step(5, TOTAL, 'Build base IPA (build-ios-base)');
488
+ const buildIosArgs = iosCfg.simulator ? ['--simulator'] : [];
489
+ const buildIosBaseBin = path.join(projectRoot, 'node_modules', '.bin', 'build-ios-base');
490
+ if (!fs.existsSync(buildIosBaseBin)) {
491
+ console.error('✗ build-ios-base binary not found — run npm install first');
492
+ process.exit(1);
493
+ }
494
+ run(buildIosBaseBin, buildIosArgs, { cwd: projectRoot });
495
+
496
+ const baseIpa = path.join(os.tmpdir(), 'pawrap-base.ipa');
497
+ if (!fs.existsSync(baseIpa)) {
498
+ console.error(`✗ IPA not found at ${baseIpa}`);
499
+ process.exit(1);
500
+ }
501
+
502
+ step(6, TOTAL, 'Copy IPA → dist/');
503
+ fs.mkdirSync(outputDir, { recursive: true });
504
+ const dest = path.join(outputDir, `${bundleIdentifier}.ipa`);
505
+ fs.copyFileSync(baseIpa, dest);
506
+ ok(`IPA: ${dest}`);
507
+ warn('IPA is unsigned — add ios.signing to wrap.config.json to produce a signed IPA.');
508
+ }
509
+
510
+ console.log(`\n Entra redirect URI (add to your app registration):`);
511
+ console.log(` msauth.${bundleIdentifier}://auth`);
512
+ }
513
+
514
+ // ─── Main ─────────────────────────────────────────────────────────────────────
515
+ console.log(`\n${'═'.repeat(60)}`);
516
+ console.log(` Power Apps Standalone Build — ${platform.toUpperCase()}`);
517
+ console.log(` Bundle ID : ${bundleIdentifier}`);
518
+ console.log(` Display : ${displayName}`);
519
+ console.log(` Version : ${version} (${versionCode})`);
520
+ console.log(` Output : ${outputDir}`);
521
+ console.log('═'.repeat(60));
522
+
523
+ try {
524
+ if (platform === 'android') {
525
+ buildAndroid();
526
+ } else {
527
+ buildIos();
528
+ }
529
+ console.log(`\n${'═'.repeat(60)}`);
530
+ console.log(' Build complete!');
531
+ console.log('═'.repeat(60) + '\n');
532
+ } catch (err) {
533
+ console.error(`\n✗ Build failed: ${err.message}`);
534
+ process.exit(1);
535
+ }
536
+