vxrn 2.0.0-beta.130.1 → 2.0.0-beta.132.1

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.
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import module from "node:module";
3
4
  import path from "node:path";
4
5
  import { pathToFileURL } from "node:url";
@@ -247,7 +248,55 @@ function patchIosPbxprojSceneDelegate(rendered, appName) {
247
248
  }
248
249
  return patched;
249
250
  }
250
- function patchIosPbxprojPushEntitlements(rendered, appName) {
251
+ function fontFileName(font) {
252
+ return path.basename(font);
253
+ }
254
+ function pbxprojId(seed) {
255
+ return createHash("sha1").update(seed).digest("hex").slice(0, 24).toUpperCase();
256
+ }
257
+ const GOOGLE_SERVICES_PLIST = "GoogleService-Info.plist";
258
+ const GOOGLE_SERVICES_VERSION = "4.4.4";
259
+ function iosResourceFiles(app) {
260
+ return [...(app.fonts ?? []).map(fontFileName), ...app.ios?.googleServicesFile ? [GOOGLE_SERVICES_PLIST] : []];
261
+ }
262
+ function patchIosPbxprojResources(rendered, appName, names) {
263
+ let patched = rendered;
264
+ for (const name of names) {
265
+ const fileRef = pbxprojId(`one-resource-file:${name}`);
266
+ const buildFile = pbxprojId(`one-resource-build:${name}`);
267
+ const fileType = name.endsWith(".plist") ? "text.plist.xml" : "file";
268
+ const edits = [
269
+ ["/* Images.xcassets in Resources */ = {isa = PBXBuildFile;", ` ${buildFile} /* ${name} in Resources */ = {isa = PBXBuildFile; fileRef = ${fileRef} /* ${name} */; };`],
270
+ ["/* Images.xcassets */ = {isa = PBXFileReference;", ` ${fileRef} /* ${name} */ = {isa = PBXFileReference; lastKnownFileType = ${fileType}; name = ${name}; path = ${appName}/${name}; sourceTree = "<group>"; };`],
271
+ ["/* Images.xcassets */,", ` ${fileRef} /* ${name} */,`],
272
+ ["/* Images.xcassets in Resources */,", ` ${buildFile} /* ${name} in Resources */,`]
273
+ ];
274
+ for (const [anchor, insertion] of edits) {
275
+ const next = insertAfterLine(patched, anchor, insertion);
276
+ if (next === patched) {
277
+ throw new Error(`[vxrn] prebuild template project.pbxproj lost its Images.xcassets anchor (${anchor})`);
278
+ }
279
+ patched = next;
280
+ }
281
+ }
282
+ return patched;
283
+ }
284
+ function copyAppResources({ root, dest, platform, app }) {
285
+ const copies = (app.fonts ?? []).map((font) => [font, platform === "ios" ? path.join(dest, app.name, fontFileName(font)) : path.join(dest, "app", "src", "main", "assets", "fonts", fontFileName(font))]);
286
+ const googleServices = platform === "ios" ? app.ios?.googleServicesFile : app.android?.googleServicesFile;
287
+ if (googleServices) {
288
+ copies.push([googleServices, platform === "ios" ? path.join(dest, app.name, GOOGLE_SERVICES_PLIST) : path.join(dest, "app", "google-services.json")]);
289
+ }
290
+ for (const [file, target] of copies) {
291
+ const source = path.resolve(root, file);
292
+ if (!FSExtra.existsSync(source)) {
293
+ throw new Error(`[vxrn] native.app names ${file}, which does not exist`);
294
+ }
295
+ FSExtra.mkdirSync(path.dirname(target), { recursive: true });
296
+ FSExtra.copyFileSync(source, target);
297
+ }
298
+ }
299
+ function patchIosPbxprojAppEntitlements(rendered, appName) {
251
300
  const edits = [["/* AppDelegate.swift */ = {isa = PBXFileReference;", ` ${PUSH_ENTITLEMENTS_FILE_REF_ID} /* ${appName}.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = ${appName}.entitlements; path = ${appName}/${appName}.entitlements; sourceTree = "<group>"; };`], ["/* AppDelegate.swift */,", ` ${PUSH_ENTITLEMENTS_FILE_REF_ID} /* ${appName}.entitlements */,`]];
252
301
  let patched = rendered;
253
302
  for (const [anchor, insertion] of edits) {
@@ -264,13 +313,55 @@ function patchIosPbxprojPushEntitlements(rendered, appName) {
264
313
  return patched.split(setting).join(`${setting}
265
314
  CODE_SIGN_ENTITLEMENTS = ${appName}/${appName}.entitlements;`);
266
315
  }
267
- function renderPushEntitlements() {
316
+ function renderPlistValue(value, indent) {
317
+ if (typeof value === "string") return `${indent}<string>${escapeXml(value)}</string>`;
318
+ if (typeof value === "boolean") return `${indent}<${value}/>`;
319
+ if (typeof value === "number") {
320
+ return Number.isInteger(value) ? `${indent}<integer>${value}</integer>` : `${indent}<real>${value}</real>`;
321
+ }
322
+ if (Array.isArray(value)) {
323
+ return `${indent}<array>
324
+ ${value.map((item) => renderPlistValue(item, `${indent} `)).join("\n")}
325
+ ${indent}</array>`;
326
+ }
327
+ return `${indent}<dict>
328
+ ${renderPlistEntries(value, `${indent} `)}
329
+ ${indent}</dict>`;
330
+ }
331
+ function renderPlistEntries(entries, indent) {
332
+ return Object.entries(entries).map(([key, value]) => `${indent}<key>${escapeXml(key)}</key>
333
+ ${renderPlistValue(value, indent)}`).join("\n");
334
+ }
335
+ function appEntitlements(app) {
336
+ const modeled = {};
337
+ if (app.notifications?.push === true || app.ios?.widgets?.pushNotifications) {
338
+ modeled["aps-environment"] = notificationsHost.apsEnvironment;
339
+ }
340
+ if (app.ios?.widgets) {
341
+ modeled["com.apple.security.application-groups"] = [app.ios.widgets.appGroup];
342
+ }
343
+ if (app.ios?.associatedDomains?.length) {
344
+ modeled["com.apple.developer.associated-domains"] = app.ios.associatedDomains;
345
+ }
346
+ if (app.ios?.usesAppleSignIn) {
347
+ modeled["com.apple.developer.applesignin"] = ["Default"];
348
+ }
349
+ for (const key of Object.keys(app.ios?.entitlements ?? {})) {
350
+ if (key in modeled) {
351
+ throw new Error(`[vxrn] native.app.ios.entitlements sets ${key}, which native.app already writes: use its field`);
352
+ }
353
+ }
354
+ return {
355
+ ...modeled,
356
+ ...app.ios?.entitlements
357
+ };
358
+ }
359
+ function renderEntitlements(entries) {
268
360
  return `<?xml version="1.0" encoding="UTF-8"?>
269
361
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
270
362
  <plist version="1.0">
271
363
  <dict>
272
- <key>aps-environment</key>
273
- <string>${notificationsHost.apsEnvironment}</string>
364
+ ${renderPlistEntries(entries, " ")}
274
365
  </dict>
275
366
  </plist>
276
367
  `;
@@ -409,8 +500,9 @@ function generateSceneDelegate(args) {
409
500
  const { dest, platform, app } = args;
410
501
  if (platform !== "ios") return;
411
502
  FSExtra.writeFileSync(path.join(dest, app.name, "SceneDelegate.swift"), renderSceneDelegateSwift(app.name));
412
- if (app.notifications?.push === true && !app.ios?.widgets) {
413
- FSExtra.writeFileSync(path.join(dest, app.name, `${app.name}.entitlements`), renderPushEntitlements());
503
+ const entitlements = appEntitlements(app);
504
+ if (Object.keys(entitlements).length && !app.ios?.widgets) {
505
+ FSExtra.writeFileSync(path.join(dest, app.name, `${app.name}.entitlements`), renderEntitlements(entitlements));
414
506
  }
415
507
  }
416
508
  function generateIosWidgets(dest, app) {
@@ -423,7 +515,7 @@ function generateIosWidgets(dest, app) {
423
515
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
424
516
  <plist version="1.0"><dict><key>com.apple.security.application-groups</key><array><string>${escapeXml(widgets.appGroup)}</string></array></dict></plist>
425
517
  `;
426
- FSExtra.writeFileSync(path.join(appDir, "OneAppWidgets.entitlements"), widgets.pushNotifications || app.notifications?.push ? entitlements.replace("</dict></plist>", `<key>aps-environment</key><string>${notificationsHost.apsEnvironment}</string></dict></plist>`) : entitlements);
518
+ FSExtra.writeFileSync(path.join(appDir, "OneAppWidgets.entitlements"), renderEntitlements(appEntitlements(app)));
427
519
  FSExtra.writeFileSync(path.join(extensionDir, "OneWidgets.entitlements"), entitlements);
428
520
  FSExtra.writeFileSync(path.join(extensionDir, "WidgetInfo.plist"), `<?xml version="1.0" encoding="UTF-8"?>
429
521
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -1009,38 +1101,85 @@ ${adaptiveIcon.monochrome ? " <monochrome android:drawable=\"@mipmap/ic_launc
1009
1101
  async function generateSplashScreen(args) {
1010
1102
  const { root, dest, platform, app } = args;
1011
1103
  if (!app.splash) return;
1012
- const source = path.resolve(root, app.splash.source);
1013
- if (!FSExtra.existsSync(source)) {
1014
- throw new Error(`[vxrn] native.app.splash source does not exist: ${source}`);
1015
- }
1016
1104
  const cover = app.splash.resizeMode === "cover";
1017
- const image = sharp(source).rotate();
1018
- const { data: artwork, info: metadata } = await (cover ? image : image.trim({ background: app.splash.backgroundColor })).png().toBuffer({ resolveWithObject: true });
1019
- if (!metadata.width || !metadata.height) {
1020
- throw new Error("[vxrn] native.app.splash source must be an image");
1021
- }
1105
+ const prepareArtwork = async (sourcePath, backgroundColor) => {
1106
+ const source = path.resolve(root, sourcePath);
1107
+ if (!FSExtra.existsSync(source)) {
1108
+ throw new Error(`[vxrn] native.app.splash source does not exist: ${source}`);
1109
+ }
1110
+ const image = sharp(source).rotate();
1111
+ const prepared = await (cover ? image : image.trim({ background: backgroundColor })).png().toBuffer({ resolveWithObject: true });
1112
+ if (!prepared.info.width || !prepared.info.height) {
1113
+ throw new Error("[vxrn] native.app.splash source must be an image");
1114
+ }
1115
+ return prepared;
1116
+ };
1117
+ const { data: artwork, info: metadata } = await prepareArtwork(app.splash.source, app.splash.backgroundColor);
1118
+ const dark = app.splash.dark;
1119
+ const darkArtwork = dark?.source ? (await prepareArtwork(dark.source, dark.backgroundColor)).data : void 0;
1022
1120
  const artworkWidth = app.splash.width ?? 200;
1023
1121
  const artworkHeight = Number((artworkWidth * (metadata.height / metadata.width)).toFixed(3));
1024
1122
  if (platform === "ios") {
1025
1123
  const appDir = path.join(dest, app.name);
1026
1124
  const splashDir = path.join(appDir, "Images.xcassets", "Splash.imageset");
1027
1125
  FSExtra.mkdirSync(splashDir, { recursive: true });
1126
+ const darkAppearance = [{
1127
+ appearance: "luminosity",
1128
+ value: "dark"
1129
+ }];
1028
1130
  await sharp(artwork).toFile(path.join(splashDir, "splash.png"));
1131
+ if (darkArtwork) await sharp(darkArtwork).toFile(path.join(splashDir, "splash-dark.png"));
1029
1132
  FSExtra.writeFileSync(path.join(splashDir, "Contents.json"), `${JSON.stringify({
1030
1133
  images: [{
1031
1134
  filename: "splash.png",
1032
1135
  idiom: "universal",
1033
1136
  scale: "1x"
1034
- }],
1137
+ }, ...darkArtwork ? [{
1138
+ appearances: darkAppearance,
1139
+ filename: "splash-dark.png",
1140
+ idiom: "universal",
1141
+ scale: "1x"
1142
+ }] : []],
1143
+ info: {
1144
+ author: "xcode",
1145
+ version: 1
1146
+ }
1147
+ }, null, 2)}
1148
+ `);
1149
+ const srgb = (hex) => ({
1150
+ red: Number.parseInt(hex.slice(1, 3), 16) / 255,
1151
+ green: Number.parseInt(hex.slice(3, 5), 16) / 255,
1152
+ blue: Number.parseInt(hex.slice(5, 7), 16) / 255
1153
+ });
1154
+ const colorEntry = (hex) => {
1155
+ const { red: red2, green: green2, blue: blue2 } = srgb(hex);
1156
+ return {
1157
+ color: {
1158
+ "color-space": "srgb",
1159
+ components: {
1160
+ red: red2.toFixed(3),
1161
+ green: green2.toFixed(3),
1162
+ blue: blue2.toFixed(3),
1163
+ alpha: "1.000"
1164
+ }
1165
+ },
1166
+ idiom: "universal"
1167
+ };
1168
+ };
1169
+ const colorDir = path.join(appDir, "Images.xcassets", "SplashBackground.colorset");
1170
+ FSExtra.mkdirSync(colorDir, { recursive: true });
1171
+ FSExtra.writeFileSync(path.join(colorDir, "Contents.json"), `${JSON.stringify({
1172
+ colors: [colorEntry(app.splash.backgroundColor), ...dark ? [{
1173
+ appearances: darkAppearance,
1174
+ ...colorEntry(dark.backgroundColor)
1175
+ }] : []],
1035
1176
  info: {
1036
1177
  author: "xcode",
1037
1178
  version: 1
1038
1179
  }
1039
1180
  }, null, 2)}
1040
1181
  `);
1041
- const red = Number.parseInt(app.splash.backgroundColor.slice(1, 3), 16) / 255;
1042
- const green = Number.parseInt(app.splash.backgroundColor.slice(3, 5), 16) / 255;
1043
- const blue = Number.parseInt(app.splash.backgroundColor.slice(5, 7), 16) / 255;
1182
+ const { red, green, blue } = srgb(app.splash.backgroundColor);
1044
1183
  FSExtra.writeFileSync(path.join(appDir, "LaunchScreen.storyboard"), `<?xml version="1.0" encoding="UTF-8"?>
1045
1184
  <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="launch-controller">
1046
1185
  <device id="retina6_12" orientation="portrait" appearance="light"/>
@@ -1058,7 +1197,7 @@ async function generateSplashScreen(args) {
1058
1197
  <subviews>
1059
1198
  <imageView userInteractionEnabled="NO" contentMode="${cover ? "scaleAspectFill" : "scaleAspectFit"}" image="Splash" translatesAutoresizingMaskIntoConstraints="NO" id="splash-image"/>
1060
1199
  </subviews>
1061
- <color key="backgroundColor" red="${red}" green="${green}" blue="${blue}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
1200
+ <color key="backgroundColor" name="SplashBackground"/>
1062
1201
  <constraints>
1063
1202
  ${cover ? ` <constraint firstItem="splash-image" firstAttribute="leading" secondItem="launch-view" secondAttribute="leading" id="splash-leading"/>
1064
1203
  <constraint firstItem="splash-image" firstAttribute="trailing" secondItem="launch-view" secondAttribute="trailing" id="splash-trailing"/>
@@ -1076,6 +1215,9 @@ ${cover ? ` <constraint firstItem="splash-image" firstAttribute="le
1076
1215
  </scenes>
1077
1216
  <resources>
1078
1217
  <image name="Splash" width="${metadata.width}" height="${metadata.height}"/>
1218
+ <namedColor name="SplashBackground">
1219
+ <color red="${red}" green="${green}" blue="${blue}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
1220
+ </namedColor>
1079
1221
  </resources>
1080
1222
  </document>
1081
1223
  `);
@@ -1083,35 +1225,39 @@ ${cover ? ` <constraint firstItem="splash-image" firstAttribute="le
1083
1225
  }
1084
1226
  const mainRes = path.join(dest, "app", "src", "main", "res");
1085
1227
  const drawable = path.join(mainRes, "drawable");
1086
- for (const [density, multiplier] of Object.entries(ANDROID_DENSITIES)) {
1087
- const canvasSize = 288 * multiplier;
1088
- const imageSize = Math.round(artworkWidth * multiplier);
1089
- const contained = await sharp(artwork).resize(imageSize, imageSize, {
1090
- fit: "contain",
1091
- background: {
1092
- r: 0,
1093
- g: 0,
1094
- b: 0,
1095
- alpha: 0
1096
- }
1097
- }).png().toBuffer();
1098
- const drawableDensity = path.join(mainRes, `drawable-${density}`);
1099
- FSExtra.mkdirSync(drawableDensity, { recursive: true });
1100
- await sharp({ create: {
1101
- width: canvasSize,
1102
- height: canvasSize,
1103
- channels: 4,
1104
- background: {
1105
- r: 0,
1106
- g: 0,
1107
- b: 0,
1108
- alpha: 0
1109
- }
1110
- } }).composite([{
1111
- input: contained,
1112
- left: Math.round((canvasSize - imageSize) / 2),
1113
- top: Math.round((canvasSize - imageSize) / 2)
1114
- }]).png().toFile(path.join(drawableDensity, "splash.png"));
1228
+ const variants = [[artwork, "drawable"]];
1229
+ if (darkArtwork) variants.push([darkArtwork, "drawable-night"]);
1230
+ for (const [variantArtwork, drawablePrefix] of variants) {
1231
+ for (const [density, multiplier] of Object.entries(ANDROID_DENSITIES)) {
1232
+ const canvasSize = 288 * multiplier;
1233
+ const imageSize = Math.round(artworkWidth * multiplier);
1234
+ const contained = await sharp(variantArtwork).resize(imageSize, imageSize, {
1235
+ fit: "contain",
1236
+ background: {
1237
+ r: 0,
1238
+ g: 0,
1239
+ b: 0,
1240
+ alpha: 0
1241
+ }
1242
+ }).png().toBuffer();
1243
+ const drawableDensity = path.join(mainRes, `${drawablePrefix}-${density}`);
1244
+ FSExtra.mkdirSync(drawableDensity, { recursive: true });
1245
+ await sharp({ create: {
1246
+ width: canvasSize,
1247
+ height: canvasSize,
1248
+ channels: 4,
1249
+ background: {
1250
+ r: 0,
1251
+ g: 0,
1252
+ b: 0,
1253
+ alpha: 0
1254
+ }
1255
+ } }).composite([{
1256
+ input: contained,
1257
+ left: Math.round((canvasSize - imageSize) / 2),
1258
+ top: Math.round((canvasSize - imageSize) / 2)
1259
+ }]).png().toFile(path.join(drawableDensity, "splash.png"));
1260
+ }
1115
1261
  }
1116
1262
  FSExtra.writeFileSync(path.join(drawable, "launch_screen.xml"), `<?xml version="1.0" encoding="utf-8"?>
1117
1263
  <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
@@ -1126,6 +1272,14 @@ ${cover ? ` <constraint firstItem="splash-image" firstAttribute="le
1126
1272
  <color name="splash_background">${app.splash.backgroundColor}</color>
1127
1273
  </resources>
1128
1274
  `);
1275
+ if (dark) {
1276
+ FSExtra.mkdirSync(path.join(mainRes, "values-night"), { recursive: true });
1277
+ FSExtra.writeFileSync(path.join(mainRes, "values-night", "colors.xml"), `<?xml version="1.0" encoding="utf-8"?>
1278
+ <resources>
1279
+ <color name="splash_background">${dark.backgroundColor}</color>
1280
+ </resources>
1281
+ `);
1282
+ }
1129
1283
  const stylesPath = path.join(mainRes, "values", "styles.xml");
1130
1284
  const styles = FSExtra.readFileSync(stylesPath, "utf8").replace(" <!-- Customize your theme here. -->", " <item name=\"android:windowBackground\">@drawable/launch_screen</item>");
1131
1285
  FSExtra.writeFileSync(stylesPath, styles);
@@ -1207,6 +1361,12 @@ ${schemes.map((scheme) => ` <string>${scheme}</string>`).join("\n")}
1207
1361
  }
1208
1362
  stamps.push(` <key>${updatesHost.runtimeVersionInfoPlistKey}</key>
1209
1363
  <string>${escapeXml(app.updates.runtimeVersion)}</string>`);
1364
+ }
1365
+ if (app.fonts?.length) {
1366
+ stamps.push(` <key>UIAppFonts</key>
1367
+ <array>
1368
+ ${app.fonts.map((font) => ` <string>${escapeXml(fontFileName(font))}</string>`).join("\n")}
1369
+ </array>`);
1210
1370
  }
1211
1371
  if (app.userInterfaceStyle !== void 0) {
1212
1372
  stamps.push(` <key>UIUserInterfaceStyle</key>
@@ -1223,6 +1383,12 @@ ${schemes.map((scheme) => ` <string>${scheme}</string>`).join("\n")}
1223
1383
  ${phoneOrientations.map((orientation) => ` <string>${orientation}</string>`).join("\n")}
1224
1384
  </array>`);
1225
1385
  }
1386
+ for (const [key, value] of Object.entries(app.ios?.infoPlist ?? {})) {
1387
+ if (rendered.includes(`<key>${key}</key>`) || stamps.some((stamp) => stamp.includes(`<key>${key}</key>`))) {
1388
+ throw new Error(`[vxrn] native.app.ios.infoPlist sets ${key}, which the template or native.app already writes: use its field`);
1389
+ }
1390
+ stamps.push(renderPlistEntries({ [key]: value }, " "));
1391
+ }
1226
1392
  if (stamps.length) {
1227
1393
  const anchor = " <key>LSRequiresIPhoneOS</key>";
1228
1394
  if (!rendered.includes(anchor)) throw new Error(`[vxrn] prebuild template ${relativePath} lost its LSRequiresIPhoneOS anchor`);
@@ -1334,6 +1500,29 @@ ${line}
1334
1500
  }
1335
1501
  rendered = rendered.replace(dayNight, app.userInterfaceStyle === "light" ? "parent=\"Theme.AppCompat.Light.NoActionBar\"" : "parent=\"Theme.AppCompat.NoActionBar\"");
1336
1502
  }
1503
+ if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && (app.android?.permissions?.length || app.android?.blockedPermissions?.length)) {
1504
+ const anchor = "<uses-permission android:name=\"android.permission.INTERNET\" />";
1505
+ if (!rendered.includes(anchor)) {
1506
+ throw new Error("[vxrn] cannot stamp permissions: expected the INTERNET permission in app/src/main/AndroidManifest.xml");
1507
+ }
1508
+ const permissionName = (name) => name.includes(".") ? name : `android.permission.${name}`;
1509
+ const lines = [...(app.android?.permissions ?? []).map((name) => ` <uses-permission android:name="${permissionName(name)}" />`), ...(app.android?.blockedPermissions ?? []).map((name) => ` <uses-permission android:name="${permissionName(name)}" tools:node="remove" />`)];
1510
+ rendered = rendered.replace(anchor, `${anchor}
1511
+ ${lines.join("\n")}`);
1512
+ if (app.android?.blockedPermissions?.length && !rendered.includes("xmlns:tools=")) {
1513
+ rendered = rendered.replace("xmlns:android=\"http://schemas.android.com/apk/res/android\"", "xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:tools=\"http://schemas.android.com/tools\"");
1514
+ }
1515
+ }
1516
+ if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.android?.appLinks?.length) {
1517
+ rendered = rendered.replace(" </activity>", ` <intent-filter android:autoVerify="true">
1518
+ <action android:name="android.intent.action.VIEW" />
1519
+ <category android:name="android.intent.category.DEFAULT" />
1520
+ <category android:name="android.intent.category.BROWSABLE" />
1521
+ <data android:scheme="https" />
1522
+ ${app.android.appLinks.map((link) => ` <data android:host="${link.host}"${link.pathPrefix ? ` android:pathPrefix="${link.pathPrefix}"` : ""} />`).join("\n")}
1523
+ </intent-filter>
1524
+ </activity>`);
1525
+ }
1337
1526
  if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.orientation !== void 0) {
1338
1527
  const anchor = "android:name=\".MainActivity\"";
1339
1528
  if (!rendered.includes(anchor)) {
@@ -1447,10 +1636,13 @@ end`);
1447
1636
  rendered = patchIosBundlePhase(rendered, app.updates?.runtimeVersion);
1448
1637
  rendered = patchIosPbxprojSceneDelegate(rendered, appName);
1449
1638
  rendered = patchIosPbxprojOneBridgingHeader(rendered, appName);
1450
- if (app.notifications?.push === true && !app.ios?.widgets) {
1451
- rendered = patchIosPbxprojPushEntitlements(rendered, appName);
1639
+ if (Object.keys(appEntitlements(app)).length && !app.ios?.widgets) {
1640
+ rendered = patchIosPbxprojAppEntitlements(rendered, appName);
1452
1641
  }
1453
1642
  if (app.ios?.widgets) rendered = patchIosPbxprojWidgets(rendered, app);
1643
+ if (iosResourceFiles(app).length) {
1644
+ rendered = patchIosPbxprojResources(rendered, appName, iosResourceFiles(app));
1645
+ }
1454
1646
  }
1455
1647
  if (platform === "ios" && relativePath === "Podfile") {
1456
1648
  if (app.ios?.ccache) rendered = `ENV['USE_CCACHE'] ||= '1'
@@ -1493,8 +1685,51 @@ use_frameworks! :linkage => :${app.ios.useFrameworks}`);
1493
1685
  }
1494
1686
  rendered = rendered.replace(hardcodedGradlePlugin, `includeBuild(new File(["node", "--print", "require('module').createRequire(require.resolve('react-native/package.json')).resolve('@react-native/gradle-plugin/package.json')"].execute(null, settingsDir).text.trim()).parentFile.canonicalPath)`);
1495
1687
  }
1496
- if (platform === "android" && app.android?.minSdk !== void 0) {
1497
- rendered = rendered.replace(/minSdkVersion = \d+/g, `minSdkVersion = ${app.android.minSdk}`);
1688
+ if (platform === "android" && relativePath === "app/build.gradle" && app.android?.minify) {
1689
+ const minify = "def enableProguardInReleaseBuilds = false";
1690
+ const minifyEnabled = "minifyEnabled enableProguardInReleaseBuilds";
1691
+ if (!rendered.includes(minify) || !rendered.includes(minifyEnabled)) {
1692
+ throw new Error("[vxrn] prebuild template app/build.gradle lost its proguard switch");
1693
+ }
1694
+ rendered = rendered.replace(minify, "def enableProguardInReleaseBuilds = true");
1695
+ if (app.android.shrinkResources) {
1696
+ rendered = rendered.replace(minifyEnabled, `${minifyEnabled}
1697
+ shrinkResources true`);
1698
+ }
1699
+ }
1700
+ if (platform === "android" && app.android?.googleServicesFile) {
1701
+ const edits = [[
1702
+ "build.gradle",
1703
+ "classpath(\"org.jetbrains.kotlin:kotlin-gradle-plugin\")",
1704
+ ` classpath("com.google.gms:google-services:${GOOGLE_SERVICES_VERSION}")`
1705
+ ], [
1706
+ "app/build.gradle",
1707
+ "apply plugin: \"com.facebook.react\"",
1708
+ "apply plugin: \"com.google.gms.google-services\""
1709
+ ]];
1710
+ for (const [file, anchor, insertion] of edits) {
1711
+ if (relativePath !== file) continue;
1712
+ const next = insertAfterLine(rendered, anchor, insertion);
1713
+ if (next === rendered) {
1714
+ throw new Error(`[vxrn] prebuild template ${file} lost its ${anchor} anchor`);
1715
+ }
1716
+ rendered = next;
1717
+ }
1718
+ }
1719
+ if (platform === "android" && relativePath === "app/proguard-rules.pro" && app.android?.proguardRules) {
1720
+ rendered = `${rendered.trimEnd()}
1721
+
1722
+ ${app.android.proguardRules.trim()}
1723
+ `;
1724
+ }
1725
+ for (const [field, setting] of [
1726
+ ["minSdk", "minSdkVersion"],
1727
+ ["targetSdk", "targetSdkVersion"],
1728
+ ["compileSdk", "compileSdkVersion"]
1729
+ ]) {
1730
+ const value = platform === "android" ? app.android?.[field] : void 0;
1731
+ if (value === void 0) continue;
1732
+ rendered = rendered.replace(new RegExp(`${setting} = \\d+`, "g"), `${setting} = ${value}`);
1498
1733
  }
1499
1734
  }
1500
1735
  return {
@@ -1562,6 +1797,12 @@ const generateForPlatform = async (root, platform, app, outDir = path.resolve(ro
1562
1797
  platform,
1563
1798
  app
1564
1799
  });
1800
+ copyAppResources({
1801
+ root,
1802
+ dest,
1803
+ platform,
1804
+ app
1805
+ });
1565
1806
  generateSceneDelegate({
1566
1807
  dest,
1567
1808
  platform,