@livx.cc/appwrap 0.40.0 → 0.40.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/appwrap",
3
- "version": "0.40.0",
3
+ "version": "0.40.1",
4
4
  "description": "Wrap any PWA into a native app with native capabilities (appwrap runtime + @livx.cc/native-kit).",
5
5
  "license": "MIT",
6
6
  "author": "Elya Livshitz",
@@ -50,11 +50,9 @@
50
50
  <key>NSAllowsLocalNetworking</key>
51
51
  <true/>
52
52
  </dict>
53
- <!-- Background audio so music-player PWAs keep playing when backgrounded /
54
- screen-locked. Remove for apps that never play audio in the background. -->
55
- <key>UIBackgroundModes</key>
56
- <array>
57
- <string>audio</string>
58
- </array>
53
+ <!-- No UIBackgroundModes by default. Declaring a mode with no matching feature is an App Store
54
+ 2.5.4 rejection, so modules stamp only what they need: push → `remote-notification`,
55
+ backgroundTask → `fetch`+`processing`, config `backgroundAudio:true` → `audio` (each creates
56
+ the key if absent). Keep the default clean; apps opt into capabilities in appwrap.config. -->
59
57
  </dict>
60
58
  </plist>
package/src/cli.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  stampPlistBackgroundTasks,
34
34
  stampPlistOrientations,
35
35
  stampPrivacyTracking,
36
+ stripEmptyBackgroundModes,
36
37
  } from './derive';
37
38
  import type { WebManifest } from './derive';
38
39
 
@@ -510,19 +511,25 @@ function stampIOSDisplayName(outDir: string, cfg: AppwrapConfig, req: NativeReqs
510
511
  );
511
512
  }
512
513
 
513
- // Remote push needs the `remote-notification` background mode. The template already ships a
514
- // UIBackgroundModes array (for `audio`), so MERGE in-place — a second <key> would be a duplicate
515
- // (invalid plist). Idempotent both ways: add when enabled+missing, strip when disabled.
516
- const iosPush = !!cfg.push?.enabled && cfg.push?.ios !== false;
514
+ // UIBackgroundModes is opt-in per module/config (the template ships none). Toggle each mode in the
515
+ // shared array — a second <key> would be a duplicate (invalid plist). Idempotent both ways: MERGE
516
+ // in-place (creating the key when absent) if wanted, strip when not.
517
517
  const bgArray = /(<key>UIBackgroundModes<\/key>\s*<array>)([\s\S]*?)(<\/array>)/;
518
- const hasRN = /<string>remote-notification<\/string>/.test(src);
519
- if (iosPush && !hasRN) {
520
- src = bgArray.test(src)
521
- ? src.replace(bgArray, (_m, open, inner, close) => `${open}${inner}\t<string>remote-notification</string>\n\t${close}`)
522
- : src.replace(/<\/dict>\s*<\/plist>\s*$/, ` <key>UIBackgroundModes</key>\n <array>\n <string>remote-notification</string>\n </array>\n</dict>\n</plist>\n`);
523
- } else if (!iosPush && hasRN) {
524
- src = src.replace(/\s*<string>remote-notification<\/string>/, '');
525
- }
518
+ const toggleBgMode = (s: string, mode: string, want: boolean): string => {
519
+ const has = new RegExp(`<string>${mode}</string>`).test(s);
520
+ if (want && !has) {
521
+ return bgArray.test(s)
522
+ ? s.replace(bgArray, (_m, open, inner, close) => `${open}${inner}\t<string>${mode}</string>\n\t${close}`)
523
+ : s.replace(/<\/dict>\s*<\/plist>\s*$/, ` <key>UIBackgroundModes</key>\n <array>\n <string>${mode}</string>\n </array>\n</dict>\n</plist>\n`);
524
+ }
525
+ if (!want && has) return s.replace(new RegExp(`\\s*<string>${mode}</string>`), '');
526
+ return s;
527
+ };
528
+ // Remote push needs `remote-notification`; apps that genuinely play audio in the background opt in
529
+ // via `backgroundAudio: true` (Apple 2.5.4 rejects `audio` without a real background-audio feature).
530
+ src = toggleBgMode(src, 'remote-notification', !!cfg.push?.enabled && cfg.push?.ios !== false);
531
+ src = toggleBgMode(src, 'audio', !!cfg.backgroundAudio);
532
+ src = stripEmptyBackgroundModes(src);
526
533
 
527
534
  writeFileSync(plist, src);
528
535
  }
package/src/config.ts CHANGED
@@ -164,6 +164,11 @@ export interface AppwrapConfig {
164
164
  * same ids are what `kit.backgroundTask.register(id, …)` / `.schedule({id})` use. No-op when absent
165
165
  * or the module is inactive. */
166
166
  backgroundTasks?: string[];
167
+ /** Opt in to the `audio` UIBackgroundMode — ONLY for apps that genuinely keep playing audio while
168
+ * backgrounded/screen-locked (music/streaming/podcast players). Off by default: declaring `audio`
169
+ * without a real background-audio feature is an App Store 2.5.4 rejection. Stamps `audio` into
170
+ * Info.plist UIBackgroundModes when true; no-op/stripped when absent. */
171
+ backgroundAudio?: boolean;
167
172
  /** Remote push (APNs/FCM). Off unless set — gating matters: an `aps-environment` entitlement on a
168
173
  * team that can't hold the Push capability (e.g. a personal team) BREAKS code signing, and the
169
174
  * handshake should honestly report `push: 'none'` on an un-provisioned build. The kit returns a raw
package/src/derive.ts CHANGED
@@ -195,20 +195,15 @@ export function stampAndroidOrientation(src: string, value: string): string {
195
195
  * `remote-notification`). `ids` empty/undefined → strips the block + removes the two modes it added.
196
196
  */
197
197
  export function stampPlistBackgroundTasks(src: string, ids: string[] | undefined): string {
198
- // 1) Always rewrite the marker block (permitted identifiers). Strip first → idempotent.
198
+ // 1) Strip the marker block first (idempotent). It is re-added LAST (step 3) so its position is
199
+ // stable regardless of whether the UIBackgroundModes array below was pre-existing or freshly
200
+ // created — otherwise the marker and a created array flip order between runs (non-idempotent).
199
201
  src = src.replace(/\s*<!-- appwrap:bgtask -->[\s\S]*?<!-- \/appwrap:bgtask -->/g, '');
200
202
  const list = (ids ?? []).filter(Boolean);
201
- if (list.length) {
202
- const items = list.map((s) => ` <string>${s}</string>`).join('\n');
203
- const block =
204
- ` <!-- appwrap:bgtask -->\n` +
205
- ` <key>BGTaskSchedulerPermittedIdentifiers</key>\n <array>\n${items}\n </array>\n` +
206
- ` <!-- /appwrap:bgtask -->`;
207
- src = src.replace(/<\/dict>\s*<\/plist>\s*$/, `${block}\n</dict>\n</plist>\n`);
208
- }
209
203
 
210
- // 2) Merge/remove the fetch + processing background modes (separate from the marker — they live in
211
- // the shared UIBackgroundModes array, which may also hold audio/remote-notification).
204
+ // 2) Merge/remove the fetch + processing background modes — they live in the shared
205
+ // UIBackgroundModes array (which may also hold audio/remote-notification). Create the key when
206
+ // absent; strip the two modes (and, via stripEmptyBackgroundModes, the emptied key) when off.
212
207
  const modes = ['fetch', 'processing'];
213
208
  const bgArray = /(<key>UIBackgroundModes<\/key>\s*<array>)([\s\S]*?)(<\/array>)/;
214
209
  if (list.length) {
@@ -225,9 +220,26 @@ export function stampPlistBackgroundTasks(src: string, ids: string[] | undefined
225
220
  } else {
226
221
  for (const m of modes) src = src.replace(new RegExp(`\\s*<string>${m}</string>`), '');
227
222
  }
223
+ src = stripEmptyBackgroundModes(src);
224
+
225
+ // 3) Re-add the permitted-identifiers marker block LAST, right before </dict> (stable position).
226
+ if (list.length) {
227
+ const items = list.map((s) => ` <string>${s}</string>`).join('\n');
228
+ const block =
229
+ ` <!-- appwrap:bgtask -->\n` +
230
+ ` <key>BGTaskSchedulerPermittedIdentifiers</key>\n <array>\n${items}\n </array>\n` +
231
+ ` <!-- /appwrap:bgtask -->`;
232
+ src = src.replace(/<\/dict>\s*<\/plist>\s*$/, `${block}\n</dict>\n</plist>\n`);
233
+ }
228
234
  return src;
229
235
  }
230
236
 
237
+ /** Remove a now-empty `UIBackgroundModes` key+array so a plist that opts into no modes carries no
238
+ * dangling empty key (keeps the default tidy). Pure + idempotent; no-op when the array has members. */
239
+ export function stripEmptyBackgroundModes(src: string): string {
240
+ return src.replace(/\s*<key>UIBackgroundModes<\/key>\s*<array>\s*<\/array>/g, '');
241
+ }
242
+
231
243
  /**
232
244
  * Stamp (or strip) `WKAppBoundDomains` in an Info.plist string — Apple's gate for running a service
233
245
  * worker inside a WKWebView (paired with `limitsNavigationsToAppBoundDomains` on the config). Pure +