@microsoft/power-apps-native-host 0.2.22 → 0.2.25
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/README.md +143 -26
- package/compatibility/current.json +88 -0
- package/lib/NOTICE.txt +146 -1226
- package/lib/commonjs/auth/AuthContext.web.js +32 -0
- package/lib/commonjs/auth/completePowerAppsAuthSession.js +16 -0
- package/lib/commonjs/bridge/NativePowerAppsBridge.js +11 -1
- package/lib/commonjs/bridge/PowerPagesBridge.js +138 -0
- package/lib/commonjs/index.js +22 -2
- package/lib/commonjs/offline/useOfflineIntegration.js +12 -0
- package/lib/commonjs/offline/useOfflineIntegration.web.js +11 -0
- package/lib/commonjs/provider/PowerAppsHostProvider.web.js +41 -0
- package/lib/commonjs/provider/PowerAppsProvider.js +67 -10
- package/lib/module/auth/AuthContext.web.js +26 -0
- package/lib/module/auth/completePowerAppsAuthSession.js +11 -0
- package/lib/module/bridge/NativePowerAppsBridge.js +11 -1
- package/lib/module/bridge/PowerPagesBridge.js +132 -0
- package/lib/module/index.js +4 -2
- package/lib/module/offline/useOfflineIntegration.js +3 -0
- package/lib/module/offline/useOfflineIntegration.web.js +7 -0
- package/lib/module/provider/PowerAppsHostProvider.web.js +36 -0
- package/lib/module/provider/PowerAppsProvider.js +66 -9
- package/lib/typescript/commonjs/auth/AuthContext.web.d.ts +20 -0
- package/lib/typescript/commonjs/auth/completePowerAppsAuthSession.d.ts +8 -0
- package/lib/typescript/commonjs/bridge/PowerPagesBridge.d.ts +18 -0
- package/lib/typescript/commonjs/index.d.ts +3 -0
- package/lib/typescript/commonjs/offline/useOfflineIntegration.d.ts +2 -0
- package/lib/typescript/commonjs/offline/useOfflineIntegration.web.d.ts +4 -0
- package/lib/typescript/commonjs/provider/PowerAppsHostProvider.web.d.ts +5 -0
- package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +2 -2
- package/lib/typescript/module/auth/AuthContext.web.d.ts +20 -0
- package/lib/typescript/module/auth/completePowerAppsAuthSession.d.ts +8 -0
- package/lib/typescript/module/bridge/PowerPagesBridge.d.ts +18 -0
- package/lib/typescript/module/index.d.ts +7 -4
- package/lib/typescript/module/offline/useOfflineIntegration.d.ts +2 -0
- package/lib/typescript/module/offline/useOfflineIntegration.web.d.ts +4 -0
- package/lib/typescript/module/provider/PowerAppsHostProvider.web.d.ts +5 -0
- package/lib/typescript/module/provider/PowerAppsProvider.d.ts +4 -4
- package/package.json +36 -16
- package/scripts/build-ios-base.js +66 -27
- package/scripts/bundle.js +212 -5
- package/scripts/generate-connector-schemas.js +8 -6
- package/scripts/start-web.mjs +137 -0
- package/scripts/upgrade/runtime.js +457 -0
- package/scripts/upgrade/system.js +244 -0
- package/scripts/upgrade-authoring.js +535 -0
- package/scripts/upgrade.js +188 -0
- package/scripts/wrap.js +536 -0
package/scripts/wrap.js
ADDED
|
@@ -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
|
+
|