vxrn 2.0.0-beta.129.1 → 2.0.0-beta.131.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.
@@ -10,6 +10,8 @@ import { swiftPackageDirectories } from "../utils/swiftPackageId.mjs";
10
10
  const nativeProjectPatches = module.createRequire(import.meta.url)("../../native-project-patches.cjs");
11
11
  const notificationsHost = nativeProjectPatches.ONE_NOTIFICATIONS;
12
12
  const updatesHost = nativeProjectPatches.ONE_UPDATES;
13
+ const PORTRAIT_ORIENTATIONS = ["UIInterfaceOrientationPortrait", "UIInterfaceOrientationPortraitUpsideDown"];
14
+ const LANDSCAPE_ORIENTATIONS = ["UIInterfaceOrientationLandscapeLeft", "UIInterfaceOrientationLandscapeRight"];
13
15
  const validatePrebuildApp = validateNativeApp;
14
16
  const ANDROID_DENSITIES = {
15
17
  mdpi: 1,
@@ -245,7 +247,7 @@ function patchIosPbxprojSceneDelegate(rendered, appName) {
245
247
  }
246
248
  return patched;
247
249
  }
248
- function patchIosPbxprojPushEntitlements(rendered, appName) {
250
+ function patchIosPbxprojAppEntitlements(rendered, appName) {
249
251
  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 */,`]];
250
252
  let patched = rendered;
251
253
  for (const [anchor, insertion] of edits) {
@@ -262,13 +264,55 @@ function patchIosPbxprojPushEntitlements(rendered, appName) {
262
264
  return patched.split(setting).join(`${setting}
263
265
  CODE_SIGN_ENTITLEMENTS = ${appName}/${appName}.entitlements;`);
264
266
  }
265
- function renderPushEntitlements() {
267
+ function renderPlistValue(value, indent) {
268
+ if (typeof value === "string") return `${indent}<string>${escapeXml(value)}</string>`;
269
+ if (typeof value === "boolean") return `${indent}<${value}/>`;
270
+ if (typeof value === "number") {
271
+ return Number.isInteger(value) ? `${indent}<integer>${value}</integer>` : `${indent}<real>${value}</real>`;
272
+ }
273
+ if (Array.isArray(value)) {
274
+ return `${indent}<array>
275
+ ${value.map((item) => renderPlistValue(item, `${indent} `)).join("\n")}
276
+ ${indent}</array>`;
277
+ }
278
+ return `${indent}<dict>
279
+ ${renderPlistEntries(value, `${indent} `)}
280
+ ${indent}</dict>`;
281
+ }
282
+ function renderPlistEntries(entries, indent) {
283
+ return Object.entries(entries).map(([key, value]) => `${indent}<key>${escapeXml(key)}</key>
284
+ ${renderPlistValue(value, indent)}`).join("\n");
285
+ }
286
+ function appEntitlements(app) {
287
+ const modeled = {};
288
+ if (app.notifications?.push === true || app.ios?.widgets?.pushNotifications) {
289
+ modeled["aps-environment"] = notificationsHost.apsEnvironment;
290
+ }
291
+ if (app.ios?.widgets) {
292
+ modeled["com.apple.security.application-groups"] = [app.ios.widgets.appGroup];
293
+ }
294
+ if (app.ios?.associatedDomains?.length) {
295
+ modeled["com.apple.developer.associated-domains"] = app.ios.associatedDomains;
296
+ }
297
+ if (app.ios?.usesAppleSignIn) {
298
+ modeled["com.apple.developer.applesignin"] = ["Default"];
299
+ }
300
+ for (const key of Object.keys(app.ios?.entitlements ?? {})) {
301
+ if (key in modeled) {
302
+ throw new Error(`[vxrn] native.app.ios.entitlements sets ${key}, which native.app already writes: use its field`);
303
+ }
304
+ }
305
+ return {
306
+ ...modeled,
307
+ ...app.ios?.entitlements
308
+ };
309
+ }
310
+ function renderEntitlements(entries) {
266
311
  return `<?xml version="1.0" encoding="UTF-8"?>
267
312
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
268
313
  <plist version="1.0">
269
314
  <dict>
270
- <key>aps-environment</key>
271
- <string>${notificationsHost.apsEnvironment}</string>
315
+ ${renderPlistEntries(entries, " ")}
272
316
  </dict>
273
317
  </plist>
274
318
  `;
@@ -407,8 +451,9 @@ function generateSceneDelegate(args) {
407
451
  const { dest, platform, app } = args;
408
452
  if (platform !== "ios") return;
409
453
  FSExtra.writeFileSync(path.join(dest, app.name, "SceneDelegate.swift"), renderSceneDelegateSwift(app.name));
410
- if (app.notifications?.push === true && !app.ios?.widgets) {
411
- FSExtra.writeFileSync(path.join(dest, app.name, `${app.name}.entitlements`), renderPushEntitlements());
454
+ const entitlements = appEntitlements(app);
455
+ if (Object.keys(entitlements).length && !app.ios?.widgets) {
456
+ FSExtra.writeFileSync(path.join(dest, app.name, `${app.name}.entitlements`), renderEntitlements(entitlements));
412
457
  }
413
458
  }
414
459
  function generateIosWidgets(dest, app) {
@@ -421,7 +466,7 @@ function generateIosWidgets(dest, app) {
421
466
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
422
467
  <plist version="1.0"><dict><key>com.apple.security.application-groups</key><array><string>${escapeXml(widgets.appGroup)}</string></array></dict></plist>
423
468
  `;
424
- 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);
469
+ FSExtra.writeFileSync(path.join(appDir, "OneAppWidgets.entitlements"), renderEntitlements(appEntitlements(app)));
425
470
  FSExtra.writeFileSync(path.join(extensionDir, "OneWidgets.entitlements"), entitlements);
426
471
  FSExtra.writeFileSync(path.join(extensionDir, "WidgetInfo.plist"), `<?xml version="1.0" encoding="UTF-8"?>
427
472
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -964,6 +1009,46 @@ async function generateAppIcons(args) {
964
1009
  }
965
1010
  }
966
1011
  }
1012
+ async function generateAdaptiveIcon(args) {
1013
+ const { root, dest, app } = args;
1014
+ const adaptiveIcon = app.android?.adaptiveIcon;
1015
+ if (!adaptiveIcon) return;
1016
+ const res = path.join(dest, "app", "src", "main", "res");
1017
+ const layers = {
1018
+ foreground: adaptiveIcon.foreground,
1019
+ background: adaptiveIcon.background,
1020
+ monochrome: adaptiveIcon.monochrome
1021
+ };
1022
+ for (const [layer, file] of Object.entries(layers)) {
1023
+ if (!file) continue;
1024
+ const source = path.resolve(root, file);
1025
+ if (!FSExtra.existsSync(source)) {
1026
+ throw new Error(`[vxrn] native.app.android.adaptiveIcon.${layer} does not exist: ${source}`);
1027
+ }
1028
+ for (const [density, multiplier] of Object.entries(ANDROID_DENSITIES)) {
1029
+ const pixels = 108 * multiplier;
1030
+ await sharp(source).rotate().resize(pixels, pixels, { fit: "cover" }).png().toFile(path.join(res, `mipmap-${density}`, `ic_launcher_${layer}.png`));
1031
+ }
1032
+ }
1033
+ if (!adaptiveIcon.background) {
1034
+ FSExtra.writeFileSync(path.join(res, "values", "ic_launcher_background.xml"), `<?xml version="1.0" encoding="utf-8"?>
1035
+ <resources>
1036
+ <color name="ic_launcher_background">${adaptiveIcon.backgroundColor ?? "#FFFFFF"}</color>
1037
+ </resources>
1038
+ `);
1039
+ }
1040
+ const adaptiveXml = `<?xml version="1.0" encoding="utf-8"?>
1041
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
1042
+ <background android:drawable="${adaptiveIcon.background ? "@mipmap/ic_launcher_background" : "@color/ic_launcher_background"}"/>
1043
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
1044
+ ${adaptiveIcon.monochrome ? " <monochrome android:drawable=\"@mipmap/ic_launcher_monochrome\"/>\n" : ""}</adaptive-icon>
1045
+ `;
1046
+ const anydpi = path.join(res, "mipmap-anydpi-v26");
1047
+ FSExtra.mkdirSync(anydpi, { recursive: true });
1048
+ for (const filename of ["ic_launcher.xml", "ic_launcher_round.xml"]) {
1049
+ FSExtra.writeFileSync(path.join(anydpi, filename), adaptiveXml);
1050
+ }
1051
+ }
967
1052
  async function generateSplashScreen(args) {
968
1053
  const { root, dest, platform, app } = args;
969
1054
  if (!app.splash) return;
@@ -971,7 +1056,9 @@ async function generateSplashScreen(args) {
971
1056
  if (!FSExtra.existsSync(source)) {
972
1057
  throw new Error(`[vxrn] native.app.splash source does not exist: ${source}`);
973
1058
  }
974
- const { data: artwork, info: metadata } = await sharp(source).rotate().trim({ background: app.splash.backgroundColor }).png().toBuffer({ resolveWithObject: true });
1059
+ 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 });
975
1062
  if (!metadata.width || !metadata.height) {
976
1063
  throw new Error("[vxrn] native.app.splash source must be an image");
977
1064
  }
@@ -1012,14 +1099,17 @@ async function generateSplashScreen(args) {
1012
1099
  <view key="view" contentMode="scaleToFill" id="launch-view">
1013
1100
  <rect key="frame" x="0.0" y="0.0" width="390" height="844"/>
1014
1101
  <subviews>
1015
- <imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" image="Splash" translatesAutoresizingMaskIntoConstraints="NO" id="splash-image"/>
1102
+ <imageView userInteractionEnabled="NO" contentMode="${cover ? "scaleAspectFill" : "scaleAspectFit"}" image="Splash" translatesAutoresizingMaskIntoConstraints="NO" id="splash-image"/>
1016
1103
  </subviews>
1017
1104
  <color key="backgroundColor" red="${red}" green="${green}" blue="${blue}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
1018
1105
  <constraints>
1019
- <constraint firstItem="splash-image" firstAttribute="centerX" secondItem="launch-view" secondAttribute="centerX" id="splash-center-x"/>
1106
+ ${cover ? ` <constraint firstItem="splash-image" firstAttribute="leading" secondItem="launch-view" secondAttribute="leading" id="splash-leading"/>
1107
+ <constraint firstItem="splash-image" firstAttribute="trailing" secondItem="launch-view" secondAttribute="trailing" id="splash-trailing"/>
1108
+ <constraint firstItem="splash-image" firstAttribute="top" secondItem="launch-view" secondAttribute="top" id="splash-top"/>
1109
+ <constraint firstItem="splash-image" firstAttribute="bottom" secondItem="launch-view" secondAttribute="bottom" id="splash-bottom"/>` : ` <constraint firstItem="splash-image" firstAttribute="centerX" secondItem="launch-view" secondAttribute="centerX" id="splash-center-x"/>
1020
1110
  <constraint firstItem="splash-image" firstAttribute="centerY" secondItem="launch-view" secondAttribute="centerY" id="splash-center-y"/>
1021
1111
  <constraint firstItem="splash-image" firstAttribute="width" constant="${artworkWidth}" id="splash-width"/>
1022
- <constraint firstItem="splash-image" firstAttribute="height" constant="${artworkHeight}" id="splash-height"/>
1112
+ <constraint firstItem="splash-image" firstAttribute="height" constant="${artworkHeight}" id="splash-height"/>`}
1023
1113
  </constraints>
1024
1114
  </view>
1025
1115
  </viewController>
@@ -1161,6 +1251,27 @@ ${schemes.map((scheme) => ` <string>${scheme}</string>`).join("\n")}
1161
1251
  stamps.push(` <key>${updatesHost.runtimeVersionInfoPlistKey}</key>
1162
1252
  <string>${escapeXml(app.updates.runtimeVersion)}</string>`);
1163
1253
  }
1254
+ if (app.userInterfaceStyle !== void 0) {
1255
+ stamps.push(` <key>UIUserInterfaceStyle</key>
1256
+ <string>${app.userInterfaceStyle === "light" ? "Light" : app.userInterfaceStyle === "dark" ? "Dark" : "Automatic"}</string>`);
1257
+ }
1258
+ if (app.orientation !== void 0) {
1259
+ const phoneOrientations = app.orientation === "portrait" ? PORTRAIT_ORIENTATIONS : app.orientation === "landscape" ? LANDSCAPE_ORIENTATIONS : [...PORTRAIT_ORIENTATIONS, ...LANDSCAPE_ORIENTATIONS];
1260
+ const phoneList = /\t<key>UISupportedInterfaceOrientations<\/key>\n\t<array>\n[\s\S]*?\t<\/array>/;
1261
+ if (!phoneList.test(rendered)) {
1262
+ throw new Error(`[vxrn] prebuild template ${relativePath} lost its UISupportedInterfaceOrientations list`);
1263
+ }
1264
+ rendered = rendered.replace(phoneList, ` <key>UISupportedInterfaceOrientations</key>
1265
+ <array>
1266
+ ${phoneOrientations.map((orientation) => ` <string>${orientation}</string>`).join("\n")}
1267
+ </array>`);
1268
+ }
1269
+ for (const [key, value] of Object.entries(app.ios?.infoPlist ?? {})) {
1270
+ if (rendered.includes(`<key>${key}</key>`) || stamps.some((stamp) => stamp.includes(`<key>${key}</key>`))) {
1271
+ throw new Error(`[vxrn] native.app.ios.infoPlist sets ${key}, which the template or native.app already writes: use its field`);
1272
+ }
1273
+ stamps.push(renderPlistEntries({ [key]: value }, " "));
1274
+ }
1164
1275
  if (stamps.length) {
1165
1276
  const anchor = " <key>LSRequiresIPhoneOS</key>";
1166
1277
  if (!rendered.includes(anchor)) throw new Error(`[vxrn] prebuild template ${relativePath} lost its LSRequiresIPhoneOS anchor`);
@@ -1265,6 +1376,44 @@ ${line}
1265
1376
  `;
1266
1377
  }
1267
1378
  }
1379
+ if (platform === "android" && relativePath === "app/src/main/res/values/styles.xml" && (app.userInterfaceStyle === "light" || app.userInterfaceStyle === "dark")) {
1380
+ const dayNight = "parent=\"Theme.AppCompat.DayNight.NoActionBar\"";
1381
+ if (!rendered.includes(dayNight)) {
1382
+ throw new Error(`[vxrn] cannot lock userInterfaceStyle: expected ${dayNight} in app/src/main/res/values/styles.xml`);
1383
+ }
1384
+ rendered = rendered.replace(dayNight, app.userInterfaceStyle === "light" ? "parent=\"Theme.AppCompat.Light.NoActionBar\"" : "parent=\"Theme.AppCompat.NoActionBar\"");
1385
+ }
1386
+ if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && (app.android?.permissions?.length || app.android?.blockedPermissions?.length)) {
1387
+ const anchor = "<uses-permission android:name=\"android.permission.INTERNET\" />";
1388
+ if (!rendered.includes(anchor)) {
1389
+ throw new Error("[vxrn] cannot stamp permissions: expected the INTERNET permission in app/src/main/AndroidManifest.xml");
1390
+ }
1391
+ const permissionName = (name) => name.includes(".") ? name : `android.permission.${name}`;
1392
+ 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" />`)];
1393
+ rendered = rendered.replace(anchor, `${anchor}
1394
+ ${lines.join("\n")}`);
1395
+ if (app.android?.blockedPermissions?.length && !rendered.includes("xmlns:tools=")) {
1396
+ 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\"");
1397
+ }
1398
+ }
1399
+ if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.android?.appLinks?.length) {
1400
+ rendered = rendered.replace(" </activity>", ` <intent-filter android:autoVerify="true">
1401
+ <action android:name="android.intent.action.VIEW" />
1402
+ <category android:name="android.intent.category.DEFAULT" />
1403
+ <category android:name="android.intent.category.BROWSABLE" />
1404
+ <data android:scheme="https" />
1405
+ ${app.android.appLinks.map((link) => ` <data android:host="${link.host}"${link.pathPrefix ? ` android:pathPrefix="${link.pathPrefix}"` : ""} />`).join("\n")}
1406
+ </intent-filter>
1407
+ </activity>`);
1408
+ }
1409
+ if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.orientation !== void 0) {
1410
+ const anchor = "android:name=\".MainActivity\"";
1411
+ if (!rendered.includes(anchor)) {
1412
+ throw new Error("[vxrn] cannot stamp the orientation: expected .MainActivity in app/src/main/AndroidManifest.xml");
1413
+ }
1414
+ rendered = rendered.replace(anchor, `${anchor}
1415
+ android:screenOrientation="${app.orientation === "default" ? "unspecified" : app.orientation}"`);
1416
+ }
1268
1417
  if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.pictureInPicture) {
1269
1418
  const anchor = "android:name=\".MainActivity\"";
1270
1419
  if (!rendered.includes(anchor)) {
@@ -1370,8 +1519,8 @@ end`);
1370
1519
  rendered = patchIosBundlePhase(rendered, app.updates?.runtimeVersion);
1371
1520
  rendered = patchIosPbxprojSceneDelegate(rendered, appName);
1372
1521
  rendered = patchIosPbxprojOneBridgingHeader(rendered, appName);
1373
- if (app.notifications?.push === true && !app.ios?.widgets) {
1374
- rendered = patchIosPbxprojPushEntitlements(rendered, appName);
1522
+ if (Object.keys(appEntitlements(app)).length && !app.ios?.widgets) {
1523
+ rendered = patchIosPbxprojAppEntitlements(rendered, appName);
1375
1524
  }
1376
1525
  if (app.ios?.widgets) rendered = patchIosPbxprojWidgets(rendered, app);
1377
1526
  }
@@ -1416,8 +1565,14 @@ use_frameworks! :linkage => :${app.ios.useFrameworks}`);
1416
1565
  }
1417
1566
  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)`);
1418
1567
  }
1419
- if (platform === "android" && app.android?.minSdk !== void 0) {
1420
- rendered = rendered.replace(/minSdkVersion = \d+/g, `minSdkVersion = ${app.android.minSdk}`);
1568
+ for (const [field, setting] of [
1569
+ ["minSdk", "minSdkVersion"],
1570
+ ["targetSdk", "targetSdkVersion"],
1571
+ ["compileSdk", "compileSdkVersion"]
1572
+ ]) {
1573
+ const value = platform === "android" ? app.android?.[field] : void 0;
1574
+ if (value === void 0) continue;
1575
+ rendered = rendered.replace(new RegExp(`${setting} = \\d+`, "g"), `${setting} = ${value}`);
1421
1576
  }
1422
1577
  }
1423
1578
  return {
@@ -1474,6 +1629,11 @@ const generateForPlatform = async (root, platform, app, outDir = path.resolve(ro
1474
1629
  platform,
1475
1630
  app
1476
1631
  });
1632
+ if (platform === "android") await generateAdaptiveIcon({
1633
+ root,
1634
+ dest,
1635
+ app
1636
+ });
1477
1637
  await generateSplashScreen({
1478
1638
  root,
1479
1639
  dest,