vxrn 2.0.0-beta.131.1 → 2.0.0-beta.133.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,6 +248,54 @@ function patchIosPbxprojSceneDelegate(rendered, appName) {
247
248
  }
248
249
  return patched;
249
250
  }
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
+ }
250
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;
@@ -286,7 +335,7 @@ ${renderPlistValue(value, indent)}`).join("\n");
286
335
  function appEntitlements(app) {
287
336
  const modeled = {};
288
337
  if (app.notifications?.push === true || app.ios?.widgets?.pushNotifications) {
289
- modeled["aps-environment"] = notificationsHost.apsEnvironment;
338
+ modeled["aps-environment"] = app.notifications?.apsEnvironment ?? notificationsHost.apsEnvironment;
290
339
  }
291
340
  if (app.ios?.widgets) {
292
341
  modeled["com.apple.security.application-groups"] = [app.ios.widgets.appGroup];
@@ -1049,41 +1098,135 @@ ${adaptiveIcon.monochrome ? " <monochrome android:drawable=\"@mipmap/ic_launc
1049
1098
  FSExtra.writeFileSync(path.join(anydpi, filename), adaptiveXml);
1050
1099
  }
1051
1100
  }
1101
+ const IOS_DARK_APPEARANCE = [{
1102
+ appearance: "luminosity",
1103
+ value: "dark"
1104
+ }];
1105
+ function srgb(hex) {
1106
+ return {
1107
+ red: Number.parseInt(hex.slice(1, 3), 16) / 255,
1108
+ green: Number.parseInt(hex.slice(3, 5), 16) / 255,
1109
+ blue: Number.parseInt(hex.slice(5, 7), 16) / 255
1110
+ };
1111
+ }
1112
+ function xcodeColorSet(light, dark) {
1113
+ const entry = (hex) => {
1114
+ const { red, green, blue } = srgb(hex);
1115
+ return {
1116
+ color: {
1117
+ "color-space": "srgb",
1118
+ components: {
1119
+ red: red.toFixed(3),
1120
+ green: green.toFixed(3),
1121
+ blue: blue.toFixed(3),
1122
+ alpha: "1.000"
1123
+ }
1124
+ },
1125
+ idiom: "universal"
1126
+ };
1127
+ };
1128
+ return `${JSON.stringify({
1129
+ colors: [entry(light), ...dark ? [{
1130
+ appearances: IOS_DARK_APPEARANCE,
1131
+ ...entry(dark)
1132
+ }] : []],
1133
+ info: {
1134
+ author: "xcode",
1135
+ version: 1
1136
+ }
1137
+ }, null, 2)}
1138
+ `;
1139
+ }
1140
+ function generateAccentColor(args) {
1141
+ const accent = args.app.ios?.accentColor;
1142
+ if (!accent) return;
1143
+ const colorDir = path.join(args.dest, args.app.name, "Images.xcassets", "AccentColor.colorset");
1144
+ FSExtra.mkdirSync(colorDir, { recursive: true });
1145
+ FSExtra.writeFileSync(path.join(colorDir, "Contents.json"), xcodeColorSet(accent.light, accent.dark));
1146
+ }
1052
1147
  async function generateSplashScreen(args) {
1053
1148
  const { root, dest, platform, app } = args;
1054
1149
  if (!app.splash) return;
1055
- const source = path.resolve(root, app.splash.source);
1056
- if (!FSExtra.existsSync(source)) {
1057
- throw new Error(`[vxrn] native.app.splash source does not exist: ${source}`);
1058
- }
1059
1150
  const cover = app.splash.resizeMode === "cover";
1060
- const image = sharp(source).rotate();
1061
- const { data: artwork, info: metadata } = await (cover ? image : image.trim({ background: app.splash.backgroundColor })).png().toBuffer({ resolveWithObject: true });
1062
- if (!metadata.width || !metadata.height) {
1063
- throw new Error("[vxrn] native.app.splash source must be an image");
1064
- }
1151
+ const prepareArtwork = async (sourcePath, backgroundColor) => {
1152
+ const source = path.resolve(root, sourcePath);
1153
+ if (!FSExtra.existsSync(source)) {
1154
+ throw new Error(`[vxrn] native.app.splash source does not exist: ${source}`);
1155
+ }
1156
+ const { width: sourceWidth, format } = await sharp(source).metadata();
1157
+ const density = format === "svg" && sourceWidth && !cover ? Math.min(2400, Math.ceil(72 * 4 * (app.splash?.width ?? 200) / sourceWidth)) : void 0;
1158
+ const image = sharp(source, density ? { density } : {}).rotate();
1159
+ const prepared = await (cover ? image : image.trim({ background: backgroundColor })).png().toBuffer({ resolveWithObject: true });
1160
+ if (!prepared.info.width || !prepared.info.height) {
1161
+ throw new Error("[vxrn] native.app.splash source must be an image");
1162
+ }
1163
+ return prepared;
1164
+ };
1165
+ const { data: artwork, info: metadata } = await prepareArtwork(app.splash.source, app.splash.backgroundColor);
1166
+ const dark = app.splash.dark;
1167
+ const darkArtwork = dark?.source ? (await prepareArtwork(dark.source, dark.backgroundColor)).data : void 0;
1065
1168
  const artworkWidth = app.splash.width ?? 200;
1066
1169
  const artworkHeight = Number((artworkWidth * (metadata.height / metadata.width)).toFixed(3));
1067
1170
  if (platform === "ios") {
1068
1171
  const appDir = path.join(dest, app.name);
1069
- const splashDir = path.join(appDir, "Images.xcassets", "Splash.imageset");
1070
- FSExtra.mkdirSync(splashDir, { recursive: true });
1071
- await sharp(artwork).toFile(path.join(splashDir, "splash.png"));
1072
- FSExtra.writeFileSync(path.join(splashDir, "Contents.json"), `${JSON.stringify({
1073
- images: [{
1074
- filename: "splash.png",
1075
- idiom: "universal",
1076
- scale: "1x"
1077
- }],
1078
- info: {
1079
- author: "xcode",
1080
- version: 1
1081
- }
1082
- }, null, 2)}
1172
+ const assets = path.join(appDir, "Images.xcassets");
1173
+ const writeImageSet = async (name, light, dark2, pointWidth) => {
1174
+ const dir = path.join(assets, `${name}.imageset`);
1175
+ FSExtra.mkdirSync(dir, { recursive: true });
1176
+ const scales = pointWidth === void 0 ? [1] : [
1177
+ 1,
1178
+ 2,
1179
+ 3
1180
+ ];
1181
+ const images = [];
1182
+ for (const [variant, suffix, appearances] of [[
1183
+ light,
1184
+ "",
1185
+ void 0
1186
+ ], [
1187
+ dark2,
1188
+ "-dark",
1189
+ IOS_DARK_APPEARANCE
1190
+ ]]) {
1191
+ if (!variant) continue;
1192
+ for (const scale of scales) {
1193
+ const filename = `${name.toLowerCase()}${suffix}${scale === 1 ? "" : `@${scale}x`}.png`;
1194
+ const output = sharp(variant);
1195
+ await (pointWidth === void 0 ? output : output.resize({ width: Math.round(pointWidth * scale) })).png().toFile(path.join(dir, filename));
1196
+ images.push({
1197
+ ...appearances ? { appearances } : {},
1198
+ filename,
1199
+ idiom: "universal",
1200
+ scale: `${scale}x`
1201
+ });
1202
+ }
1203
+ }
1204
+ FSExtra.writeFileSync(path.join(dir, "Contents.json"), `${JSON.stringify({
1205
+ images,
1206
+ info: {
1207
+ author: "xcode",
1208
+ version: 1
1209
+ }
1210
+ }, null, 2)}
1083
1211
  `);
1084
- const red = Number.parseInt(app.splash.backgroundColor.slice(1, 3), 16) / 255;
1085
- const green = Number.parseInt(app.splash.backgroundColor.slice(3, 5), 16) / 255;
1086
- const blue = Number.parseInt(app.splash.backgroundColor.slice(5, 7), 16) / 255;
1212
+ };
1213
+ await writeImageSet("Splash", artwork, darkArtwork, cover ? void 0 : artworkWidth);
1214
+ const readBackground = (sourcePath) => {
1215
+ const source = path.resolve(root, sourcePath);
1216
+ if (!FSExtra.existsSync(source)) {
1217
+ throw new Error(`[vxrn] native.app.splash backgroundImage does not exist: ${source}`);
1218
+ }
1219
+ return sharp(source).rotate().png().toBuffer({ resolveWithObject: true });
1220
+ };
1221
+ const background = app.splash.backgroundImage ? await readBackground(app.splash.backgroundImage) : void 0;
1222
+ const darkBackground = dark?.backgroundImage ? (await readBackground(dark.backgroundImage)).data : void 0;
1223
+ if (background) {
1224
+ await writeImageSet("SplashBackgroundImage", background.data, darkBackground, void 0);
1225
+ }
1226
+ const colorDir = path.join(assets, "SplashBackground.colorset");
1227
+ FSExtra.mkdirSync(colorDir, { recursive: true });
1228
+ FSExtra.writeFileSync(path.join(colorDir, "Contents.json"), xcodeColorSet(app.splash.backgroundColor, dark?.backgroundColor));
1229
+ const { red, green, blue } = srgb(app.splash.backgroundColor);
1087
1230
  FSExtra.writeFileSync(path.join(appDir, "LaunchScreen.storyboard"), `<?xml version="1.0" encoding="UTF-8"?>
1088
1231
  <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">
1089
1232
  <device id="retina6_12" orientation="portrait" appearance="light"/>
@@ -1098,11 +1241,16 @@ async function generateSplashScreen(args) {
1098
1241
  <viewController id="launch-controller" sceneMemberID="viewController">
1099
1242
  <view key="view" contentMode="scaleToFill" id="launch-view">
1100
1243
  <rect key="frame" x="0.0" y="0.0" width="390" height="844"/>
1101
- <subviews>
1244
+ <subviews>${background ? `
1245
+ <imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" image="SplashBackgroundImage" translatesAutoresizingMaskIntoConstraints="NO" id="splash-background"/>` : ""}
1102
1246
  <imageView userInteractionEnabled="NO" contentMode="${cover ? "scaleAspectFill" : "scaleAspectFit"}" image="Splash" translatesAutoresizingMaskIntoConstraints="NO" id="splash-image"/>
1103
1247
  </subviews>
1104
- <color key="backgroundColor" red="${red}" green="${green}" blue="${blue}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
1105
- <constraints>
1248
+ <color key="backgroundColor" name="SplashBackground"/>
1249
+ <constraints>${background ? `
1250
+ <constraint firstItem="splash-background" firstAttribute="leading" secondItem="launch-view" secondAttribute="leading" id="splash-background-leading"/>
1251
+ <constraint firstItem="splash-background" firstAttribute="trailing" secondItem="launch-view" secondAttribute="trailing" id="splash-background-trailing"/>
1252
+ <constraint firstItem="splash-background" firstAttribute="top" secondItem="launch-view" secondAttribute="top" id="splash-background-top"/>
1253
+ <constraint firstItem="splash-background" firstAttribute="bottom" secondItem="launch-view" secondAttribute="bottom" id="splash-background-bottom"/>` : ""}
1106
1254
  ${cover ? ` <constraint firstItem="splash-image" firstAttribute="leading" secondItem="launch-view" secondAttribute="leading" id="splash-leading"/>
1107
1255
  <constraint firstItem="splash-image" firstAttribute="trailing" secondItem="launch-view" secondAttribute="trailing" id="splash-trailing"/>
1108
1256
  <constraint firstItem="splash-image" firstAttribute="top" secondItem="launch-view" secondAttribute="top" id="splash-top"/>
@@ -1118,7 +1266,11 @@ ${cover ? ` <constraint firstItem="splash-image" firstAttribute="le
1118
1266
  </scene>
1119
1267
  </scenes>
1120
1268
  <resources>
1121
- <image name="Splash" width="${metadata.width}" height="${metadata.height}"/>
1269
+ <image name="Splash" width="${cover ? metadata.width : artworkWidth}" height="${cover ? metadata.height : artworkHeight}"/>${background ? `
1270
+ <image name="SplashBackgroundImage" width="${background.info.width}" height="${background.info.height}"/>` : ""}
1271
+ <namedColor name="SplashBackground">
1272
+ <color red="${red}" green="${green}" blue="${blue}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
1273
+ </namedColor>
1122
1274
  </resources>
1123
1275
  </document>
1124
1276
  `);
@@ -1126,35 +1278,39 @@ ${cover ? ` <constraint firstItem="splash-image" firstAttribute="le
1126
1278
  }
1127
1279
  const mainRes = path.join(dest, "app", "src", "main", "res");
1128
1280
  const drawable = path.join(mainRes, "drawable");
1129
- for (const [density, multiplier] of Object.entries(ANDROID_DENSITIES)) {
1130
- const canvasSize = 288 * multiplier;
1131
- const imageSize = Math.round(artworkWidth * multiplier);
1132
- const contained = await sharp(artwork).resize(imageSize, imageSize, {
1133
- fit: "contain",
1134
- background: {
1135
- r: 0,
1136
- g: 0,
1137
- b: 0,
1138
- alpha: 0
1139
- }
1140
- }).png().toBuffer();
1141
- const drawableDensity = path.join(mainRes, `drawable-${density}`);
1142
- FSExtra.mkdirSync(drawableDensity, { recursive: true });
1143
- await sharp({ create: {
1144
- width: canvasSize,
1145
- height: canvasSize,
1146
- channels: 4,
1147
- background: {
1148
- r: 0,
1149
- g: 0,
1150
- b: 0,
1151
- alpha: 0
1152
- }
1153
- } }).composite([{
1154
- input: contained,
1155
- left: Math.round((canvasSize - imageSize) / 2),
1156
- top: Math.round((canvasSize - imageSize) / 2)
1157
- }]).png().toFile(path.join(drawableDensity, "splash.png"));
1281
+ const variants = [[artwork, "drawable"]];
1282
+ if (darkArtwork) variants.push([darkArtwork, "drawable-night"]);
1283
+ for (const [variantArtwork, drawablePrefix] of variants) {
1284
+ for (const [density, multiplier] of Object.entries(ANDROID_DENSITIES)) {
1285
+ const canvasSize = 288 * multiplier;
1286
+ const imageSize = Math.round(artworkWidth * multiplier);
1287
+ const contained = await sharp(variantArtwork).resize(imageSize, imageSize, {
1288
+ fit: "contain",
1289
+ background: {
1290
+ r: 0,
1291
+ g: 0,
1292
+ b: 0,
1293
+ alpha: 0
1294
+ }
1295
+ }).png().toBuffer();
1296
+ const drawableDensity = path.join(mainRes, `${drawablePrefix}-${density}`);
1297
+ FSExtra.mkdirSync(drawableDensity, { recursive: true });
1298
+ await sharp({ create: {
1299
+ width: canvasSize,
1300
+ height: canvasSize,
1301
+ channels: 4,
1302
+ background: {
1303
+ r: 0,
1304
+ g: 0,
1305
+ b: 0,
1306
+ alpha: 0
1307
+ }
1308
+ } }).composite([{
1309
+ input: contained,
1310
+ left: Math.round((canvasSize - imageSize) / 2),
1311
+ top: Math.round((canvasSize - imageSize) / 2)
1312
+ }]).png().toFile(path.join(drawableDensity, "splash.png"));
1313
+ }
1158
1314
  }
1159
1315
  FSExtra.writeFileSync(path.join(drawable, "launch_screen.xml"), `<?xml version="1.0" encoding="utf-8"?>
1160
1316
  <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
@@ -1169,6 +1325,14 @@ ${cover ? ` <constraint firstItem="splash-image" firstAttribute="le
1169
1325
  <color name="splash_background">${app.splash.backgroundColor}</color>
1170
1326
  </resources>
1171
1327
  `);
1328
+ if (dark) {
1329
+ FSExtra.mkdirSync(path.join(mainRes, "values-night"), { recursive: true });
1330
+ FSExtra.writeFileSync(path.join(mainRes, "values-night", "colors.xml"), `<?xml version="1.0" encoding="utf-8"?>
1331
+ <resources>
1332
+ <color name="splash_background">${dark.backgroundColor}</color>
1333
+ </resources>
1334
+ `);
1335
+ }
1172
1336
  const stylesPath = path.join(mainRes, "values", "styles.xml");
1173
1337
  const styles = FSExtra.readFileSync(stylesPath, "utf8").replace(" <!-- Customize your theme here. -->", " <item name=\"android:windowBackground\">@drawable/launch_screen</item>");
1174
1338
  FSExtra.writeFileSync(stylesPath, styles);
@@ -1223,6 +1387,9 @@ ${schemes.map((scheme) => ` <string>${scheme}</string>`).join("\n")}
1223
1387
  stamps.push(` <key>ITSAppUsesNonExemptEncryption</key>
1224
1388
  <${app.ios.usesNonExemptEncryption ? "true" : "false"}/>`);
1225
1389
  }
1390
+ if (app.ios?.accentColor) {
1391
+ stamps.push(" <key>NSAccentColorName</key>\n <string>AccentColor</string>");
1392
+ }
1226
1393
  if (app.ios?.fileSharing) {
1227
1394
  stamps.push(` <key>UIFileSharingEnabled</key>
1228
1395
  <true/>
@@ -1250,6 +1417,12 @@ ${schemes.map((scheme) => ` <string>${scheme}</string>`).join("\n")}
1250
1417
  }
1251
1418
  stamps.push(` <key>${updatesHost.runtimeVersionInfoPlistKey}</key>
1252
1419
  <string>${escapeXml(app.updates.runtimeVersion)}</string>`);
1420
+ }
1421
+ if (app.fonts?.length) {
1422
+ stamps.push(` <key>UIAppFonts</key>
1423
+ <array>
1424
+ ${app.fonts.map((font) => ` <string>${escapeXml(fontFileName(font))}</string>`).join("\n")}
1425
+ </array>`);
1253
1426
  }
1254
1427
  if (app.userInterfaceStyle !== void 0) {
1255
1428
  stamps.push(` <key>UIUserInterfaceStyle</key>
@@ -1523,6 +1696,9 @@ end`);
1523
1696
  rendered = patchIosPbxprojAppEntitlements(rendered, appName);
1524
1697
  }
1525
1698
  if (app.ios?.widgets) rendered = patchIosPbxprojWidgets(rendered, app);
1699
+ if (iosResourceFiles(app).length) {
1700
+ rendered = patchIosPbxprojResources(rendered, appName, iosResourceFiles(app));
1701
+ }
1526
1702
  }
1527
1703
  if (platform === "ios" && relativePath === "Podfile") {
1528
1704
  if (app.ios?.ccache) rendered = `ENV['USE_CCACHE'] ||= '1'
@@ -1565,6 +1741,43 @@ use_frameworks! :linkage => :${app.ios.useFrameworks}`);
1565
1741
  }
1566
1742
  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)`);
1567
1743
  }
1744
+ if (platform === "android" && relativePath === "app/build.gradle" && app.android?.minify) {
1745
+ const minify = "def enableProguardInReleaseBuilds = false";
1746
+ const minifyEnabled = "minifyEnabled enableProguardInReleaseBuilds";
1747
+ if (!rendered.includes(minify) || !rendered.includes(minifyEnabled)) {
1748
+ throw new Error("[vxrn] prebuild template app/build.gradle lost its proguard switch");
1749
+ }
1750
+ rendered = rendered.replace(minify, "def enableProguardInReleaseBuilds = true");
1751
+ if (app.android.shrinkResources) {
1752
+ rendered = rendered.replace(minifyEnabled, `${minifyEnabled}
1753
+ shrinkResources true`);
1754
+ }
1755
+ }
1756
+ if (platform === "android" && app.android?.googleServicesFile) {
1757
+ const edits = [[
1758
+ "build.gradle",
1759
+ "classpath(\"org.jetbrains.kotlin:kotlin-gradle-plugin\")",
1760
+ ` classpath("com.google.gms:google-services:${GOOGLE_SERVICES_VERSION}")`
1761
+ ], [
1762
+ "app/build.gradle",
1763
+ "apply plugin: \"com.facebook.react\"",
1764
+ "apply plugin: \"com.google.gms.google-services\""
1765
+ ]];
1766
+ for (const [file, anchor, insertion] of edits) {
1767
+ if (relativePath !== file) continue;
1768
+ const next = insertAfterLine(rendered, anchor, insertion);
1769
+ if (next === rendered) {
1770
+ throw new Error(`[vxrn] prebuild template ${file} lost its ${anchor} anchor`);
1771
+ }
1772
+ rendered = next;
1773
+ }
1774
+ }
1775
+ if (platform === "android" && relativePath === "app/proguard-rules.pro" && app.android?.proguardRules) {
1776
+ rendered = `${rendered.trimEnd()}
1777
+
1778
+ ${app.android.proguardRules.trim()}
1779
+ `;
1780
+ }
1568
1781
  for (const [field, setting] of [
1569
1782
  ["minSdk", "minSdkVersion"],
1570
1783
  ["targetSdk", "targetSdkVersion"],
@@ -1640,6 +1853,16 @@ const generateForPlatform = async (root, platform, app, outDir = path.resolve(ro
1640
1853
  platform,
1641
1854
  app
1642
1855
  });
1856
+ if (platform === "ios") generateAccentColor({
1857
+ dest,
1858
+ app
1859
+ });
1860
+ copyAppResources({
1861
+ root,
1862
+ dest,
1863
+ platform,
1864
+ app
1865
+ });
1643
1866
  generateSceneDelegate({
1644
1867
  dest,
1645
1868
  platform,