craft-native 0.0.120 → 0.0.122

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.
@@ -79,6 +79,18 @@ function renderAppearance(config) {
79
79
  return { interfaceStyle: "Light", statusBarStyle: "UIStatusBarStyleDarkContent" };
80
80
  return { interfaceStyle: "Dark", statusBarStyle: "UIStatusBarStyleLightContent" };
81
81
  }
82
+ var LINKED_PURPOSES = {
83
+ NSSpeechRecognitionUsageDescription: "speech recognition",
84
+ NSMicrophoneUsageDescription: "the microphone",
85
+ NSCameraUsageDescription: "the camera",
86
+ NSPhotoLibraryUsageDescription: "your photo library",
87
+ NSLocationWhenInUseUsageDescription: "your location",
88
+ NSContactsUsageDescription: "your contacts",
89
+ NSCalendarsUsageDescription: "your calendar",
90
+ NSBluetoothAlwaysUsageDescription: "Bluetooth",
91
+ NSMotionUsageDescription: "motion data",
92
+ NSFaceIDUsageDescription: "Face ID"
93
+ };
82
94
  function renderUsageDescriptions(config) {
83
95
  const entries = [
84
96
  [config.enableSpeechRecognition, "NSSpeechRecognitionUsageDescription", `${config.appName} uses speech recognition for voice input.`],
@@ -97,7 +109,12 @@ function renderUsageDescriptions(config) {
97
109
  [config.enableHealthKit, "NSHealthUpdateUsageDescription", `${config.appName} writes health data only with your permission.`],
98
110
  [config.enableBiometric, "NSFaceIDUsageDescription", `${config.appName} uses Face ID to protect your account.`]
99
111
  ];
100
- return entries.filter(([enabled]) => enabled).map(([, key, value]) => plistString(key, value)).join(`
112
+ return entries.map(([enabled, key, value]) => {
113
+ if (enabled)
114
+ return [key, value];
115
+ const linked = LINKED_PURPOSES[key];
116
+ return linked ? [key, `${config.appName} asks for ${linked} only in a feature that needs it.`] : null;
117
+ }).filter((entry) => entry !== null).map(([key, value]) => plistString(key, value)).join(`
101
118
  `);
102
119
  }
103
120
  function renderOrientations(config) {
@@ -275,6 +292,41 @@ ${accessedXml}
275
292
  </plist>
276
293
  `;
277
294
  }
295
+ function pngHasAlpha(bytes) {
296
+ const signature = [137, 80, 78, 71, 13, 10, 26, 10];
297
+ if (bytes.length < 33 || signature.some((byte, index) => bytes[index] !== byte))
298
+ return false;
299
+ const colourType = bytes[25];
300
+ if (colourType === 4 || colourType === 6)
301
+ return true;
302
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
303
+ for (let offset = 8;offset + 8 <= bytes.length; ) {
304
+ const length = view.getUint32(offset);
305
+ const type = String.fromCharCode(bytes[offset + 4], bytes[offset + 5], bytes[offset + 6], bytes[offset + 7]);
306
+ if (type === "tRNS")
307
+ return true;
308
+ if (type === "IDAT" || type === "IEND")
309
+ return false;
310
+ offset += 12 + length;
311
+ }
312
+ return false;
313
+ }
314
+ function writeOpaqueIcon(source, target) {
315
+ const bytes = new Uint8Array(readFileSync(source));
316
+ if (!pngHasAlpha(bytes) || process.platform !== "darwin") {
317
+ cpSync(source, target);
318
+ return;
319
+ }
320
+ const flattened = `${target}.flatten.jpg`;
321
+ const run = (args) => Bun.spawnSync(["sips", ...args], { stdout: "ignore", stderr: "pipe" });
322
+ const toJpeg = run(["-s", "format", "jpeg", "-s", "formatOptions", "100", source, "--out", flattened]);
323
+ const toPng = toJpeg.exitCode === 0 ? run(["-s", "format", "png", flattened, "--out", target]) : toJpeg;
324
+ rmSync(flattened, { force: true });
325
+ if (toPng.exitCode !== 0) {
326
+ cpSync(source, target);
327
+ console.warn(`[craft] Could not remove the app icon's alpha channel; App Store Connect will refuse it: ${toPng.stderr.toString().trim()}`);
328
+ }
329
+ }
278
330
  function renderAssetCatalog(output, config) {
279
331
  const catalog = join(output, "Assets.xcassets");
280
332
  const appIcon = join(catalog, "AppIcon.appiconset");
@@ -287,7 +339,7 @@ function renderAssetCatalog(output, config) {
287
339
  if (config.appIconPath) {
288
340
  if (!existsSync(config.appIconPath))
289
341
  throw new Error(`App icon not found: ${config.appIconPath}`);
290
- cpSync(config.appIconPath, join(appIcon, iconFilename));
342
+ writeOpaqueIcon(config.appIconPath, join(appIcon, iconFilename));
291
343
  }
292
344
  writeFileSync(join(appIcon, "Contents.json"), `${JSON.stringify({
293
345
  images: iconFilename ? [{
@@ -437,7 +489,7 @@ async function init(options) {
437
489
  cpSync(join(TEMPLATES_DIR, "CraftNativeActions.swift"), join(output, "Sources", "CraftNativeActions.swift"));
438
490
  cpSync(join(TEMPLATES_DIR, "CraftNativeMutation.swift"), join(output, "Sources", "CraftNativeMutation.swift"));
439
491
  const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
440
- const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{APP_BOUND_DOMAINS\}\}/g, renderAppBoundDomains(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
492
+ const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{APP_BOUND_DOMAINS\}\}/g, renderAppBoundDomains(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{NON_EXEMPT_ENCRYPTION\}\}/g, config.usesNonExemptEncryption ? "true" : "false").replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
441
493
  <true/>
442
494
  <key>NSSupportsLiveActivitiesFrequentUpdates</key>
443
495
  <true/>` : "");
@@ -758,6 +810,7 @@ export {
758
810
  renderAppearance,
759
811
  renderAppBoundDomains,
760
812
  productName,
813
+ pngHasAlpha,
761
814
  pickSimulator,
762
815
  orderSimulators,
763
816
  open,