@mpgd/cli 0.58.0 → 0.61.0

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/dist/index.js CHANGED
@@ -33,7 +33,10 @@ import { runMicrosoftStoreSubmissionPreflight } from './microsoft-store-submissi
33
33
  import { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, runOfflinePlaytestPackaging, } from './offline-playtest.js';
34
34
  import { targetConfigExtensionsFileEnv, targetConfigMatrixFileEnv } from './target-config-env.js';
35
35
  import { readStoreCredential, runNativeDeployment } from './native-deploy-run.js';
36
- import { submitRecordedNativeTarget } from './native-deploy-submission.js';
36
+ import { isNativeDeployApproved, planRecordedAndroidSubmission, submitRecordedNativeTarget, } from './native-deploy-submission.js';
37
+ import { createPlayReleaseRequest, parsePlayUserFraction, readPlayReleaseNotesFile, } from './play-release-options.js';
38
+ import { promotePlayRelease } from './play-release-promote.js';
39
+ import { changePlayRollout } from './play-release-rollout.js';
37
40
  import { readNativeReleaseStatus as readNativeReleaseStatusForCli } from './release-state.js';
38
41
  export { renderGameAcceptanceMarkdown, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, defaultGameAcceptanceCommandTimeoutMs, maximumGameplayE2EReportBytes, } from './game-acceptance.js';
39
42
  export { maximumPerformanceEvidenceBytes, preparePerformanceAcceptance, readPerformanceAcceptanceEvidence, validatePerformanceBudget, } from './performance-acceptance.js';
@@ -43,8 +46,11 @@ export { installPinnedReleaseDependencies, pinNativeDeploymentPlan, preparePinne
43
46
  export { withAndroidUploadSigningSession, } from './android-signing-session.js';
44
47
  export { checkpointNativeSubmission, readNativeReleaseStatus, reclaimNativeSubmission, releaseNativeSubmissionLease, recordNativeReleaseBuild, reserveNativeRelease, } from './release-state.js';
45
48
  export { runNativeDeployment, readStoreCredential, } from './native-deploy-run.js';
46
- export { submitRecordedNativeTarget, } from './native-deploy-submission.js';
47
- export { PlaySubmissionUncertainError, submitVerifiedAndroidBundle, } from './play-internal-submission.js';
49
+ export { planRecordedAndroidSubmission, submitRecordedNativeTarget, } from './native-deploy-submission.js';
50
+ export { PlaySubmissionUncertainError, planVerifiedAndroidBundleSubmission, submitVerifiedAndroidBundle, } from './play-internal-submission.js';
51
+ export { createPlayReleaseRequest, defaultPlayReleaseRequest, maximumPlayReleaseNoteCharacters, parsePlayReleaseNotes, } from './play-release-options.js';
52
+ export { PlayPromoteUncertainError, promotePlayRelease, } from './play-release-promote.js';
53
+ export { PlayRolloutUncertainError, changePlayRollout, } from './play-release-rollout.js';
48
54
  export { IosSubmissionUncertainError, submitVerifiedIosBuild, } from './testflight-submission.js';
49
55
  export { createBrowserGameplayE2EDriver, } from './browser-gameplay-e2e.js';
50
56
  export { collectGameplayE2EPathEvidence, defaultGameplayE2EReportFile, maximumGameplayE2EStates, parseGameplayE2EPlan, readGameplayE2EPlan, renderGameplayE2EMarkdown, resolveGameplayE2EReportFile, runGameplayE2E, } from './gameplay-e2e.js';
@@ -446,7 +452,35 @@ const deployCommand = defineI18n({
446
452
  'game-id': { type: 'string', required: true, description: 'Explicit release ledger game ID.' },
447
453
  release: { type: 'string', required: true, description: 'Explicit release ID.' },
448
454
  target: { type: 'string', required: true, description: 'android or ios.' },
449
- approve: { type: 'boolean', required: false, description: 'Approve internal test submission.' },
455
+ approve: { type: 'boolean', required: false, description: 'Approve the store submission; needed for any Play track but internal.' },
456
+ track: {
457
+ type: 'string', required: false,
458
+ description: 'Android: Google Play track (internal, alpha, beta, production or a closed-testing track name); defaults to internal.',
459
+ },
460
+ status: {
461
+ type: 'string', required: false,
462
+ description: 'Android: release status draft, completed (default) or inProgress (staged rollout).',
463
+ },
464
+ 'user-fraction': {
465
+ type: 'string', required: false,
466
+ description: 'Android: staged rollout fraction for --status inProgress, greater than 0 and less than 1.',
467
+ },
468
+ 'release-notes': {
469
+ type: 'string', required: false,
470
+ description: 'Android: release notes file, Play Console <en-US>…</en-US> blocks or JSON; 500 characters per language.',
471
+ },
472
+ 'confirm-production': {
473
+ type: 'boolean', required: false,
474
+ description: 'Android: confirm a production release or staged rollout.',
475
+ },
476
+ 'mapping-file': {
477
+ type: 'string', required: false,
478
+ description: 'Android: R8 mapping.txt of this exact build, uploaded as its deobfuscation file.',
479
+ },
480
+ 'dry-run': {
481
+ type: 'boolean', required: false,
482
+ description: 'Android: verify the build and print the planned Play edit without any store call.',
483
+ },
450
484
  },
451
485
  run: async (ctx) => {
452
486
  const game = readOptionalString(ctx.values.game);
@@ -455,12 +489,136 @@ const deployCommand = defineI18n({
455
489
  if (target !== 'android' && target !== 'ios') {
456
490
  throw new Error('--target must be android or ios.');
457
491
  }
492
+ const androidRelease = await readPlayReleaseOptions(ctx.values, target === 'android');
493
+ const gameId = readRequiredCliOption(ctx.values['game-id'], '--game-id');
494
+ const releaseKey = readRequiredCliOption(ctx.values.release, '--release');
495
+ if (ctx.values['dry-run'] === true) {
496
+ if (target !== 'android') {
497
+ throw new Error('--dry-run is available for --target android.');
498
+ }
499
+ const planned = await planRecordedAndroidSubmission({
500
+ plan, gameId, releaseKey,
501
+ ...(androidRelease === undefined ? {} : { androidRelease }),
502
+ });
503
+ console.info(JSON.stringify(planned, null, 2));
504
+ return;
505
+ }
458
506
  const result = await submitRecordedNativeTarget({
459
507
  plan,
460
- gameId: readRequiredCliOption(ctx.values['game-id'], '--game-id'),
461
- releaseKey: readRequiredCliOption(ctx.values.release, '--release'),
508
+ gameId,
509
+ releaseKey,
462
510
  credential: readStoreCredential(plan, target, process.env),
463
- approved: isDeployApproved(plan, ctx.values.approve === true),
511
+ approved: isNativeDeployApproved(plan, ctx.values.approve === true, androidRelease),
512
+ ...(androidRelease === undefined ? {} : { androidRelease }),
513
+ ...(ctx.values['confirm-production'] === true ? { confirmProduction: true } : {}),
514
+ onDraftAppFallback: reportPlayDraftAppFallback,
515
+ });
516
+ console.info(JSON.stringify(result, null, 2));
517
+ },
518
+ }),
519
+ rollout: defineI18n({
520
+ name: 'rollout',
521
+ description: 'Update, halt, resume or complete a Google Play release already on a track.',
522
+ resource: commandResource({
523
+ en: 'Update, halt, resume or complete a Google Play release already on a track.',
524
+ ko: '트랙에 이미 있는 Google Play 릴리스의 배포 비율을 변경·중지·재개·완료합니다.',
525
+ }),
526
+ args: {
527
+ plan: { type: 'string', required: true, description: 'Saved deployment plan JSON (package ID and credential).' },
528
+ game: { type: 'string', required: false, description: 'Current game directory if the plan moved.' },
529
+ track: { type: 'string', required: true, description: 'Google Play track whose release changes; no other track is touched.' },
530
+ action: { type: 'string', required: true, description: 'update, halt, resume or complete.' },
531
+ 'version-code': { type: 'string', required: false, description: 'Release to change; defaults to the only release the action applies to.' },
532
+ 'user-fraction': { type: 'string', required: false, description: 'New rollout fraction for update (or resume), greater than 0 and less than 1.' },
533
+ approve: { type: 'boolean', required: false, description: 'Approve the rollout change.' },
534
+ 'confirm-production': { type: 'boolean', required: false, description: 'Confirm a production change or a running staged rollout.' },
535
+ 'dry-run': { type: 'boolean', required: false, description: 'Read the track and print the planned update; the edit is discarded.' },
536
+ },
537
+ run: async (ctx) => {
538
+ const game = readOptionalString(ctx.values.game);
539
+ const plan = readNativeDeploymentPlan(readRequiredCliOption(ctx.values.plan, '--plan'), game === undefined ? undefined : path.resolve(game));
540
+ const android = plan.targets.find((entry) => entry.target === 'android');
541
+ if (android === undefined) {
542
+ throw new Error('The deployment plan has no Android target.');
543
+ }
544
+ const dryRun = ctx.values['dry-run'] === true;
545
+ if (!dryRun && ctx.values.approve !== true) {
546
+ throw new Error('Changing a Google Play rollout requires --approve.');
547
+ }
548
+ const action = readRequiredCliOption(ctx.values.action, '--action');
549
+ const versionCode = readOptionalString(ctx.values['version-code']);
550
+ const userFraction = readOptionalString(ctx.values['user-fraction']);
551
+ if (versionCode !== undefined && !/^[1-9]\d{0,15}$/u.test(versionCode)) {
552
+ throw new Error('--version-code must be a positive integer.');
553
+ }
554
+ const credential = readStoreCredential(plan, 'android', process.env);
555
+ if (credential.target !== 'android') {
556
+ throw new Error('Google Play rollout needs the Android submission credential.');
557
+ }
558
+ const result = await changePlayRollout({
559
+ packageName: android.appId,
560
+ serviceAccountFile: credential.serviceAccountFile,
561
+ track: readRequiredCliOption(ctx.values.track, '--track'),
562
+ action,
563
+ ...(versionCode === undefined ? {} : { versionCode: Number(versionCode) }),
564
+ ...(userFraction === undefined ? {} : { userFraction: parsePlayUserFraction(userFraction) }),
565
+ ...(ctx.values['confirm-production'] === true ? { confirmProduction: true } : {}),
566
+ dryRun,
567
+ });
568
+ console.info(JSON.stringify(result, null, 2));
569
+ },
570
+ }),
571
+ promote: defineI18n({
572
+ name: 'promote',
573
+ description: 'Promote a Google Play release from one track to another without uploading.',
574
+ resource: commandResource({
575
+ en: 'Promote a Google Play release from one track to another without uploading.',
576
+ ko: '업로드 없이 Google Play 릴리스를 다른 트랙으로 승격합니다.',
577
+ }),
578
+ args: {
579
+ plan: { type: 'string', required: true, description: 'Saved deployment plan JSON (package ID and credential).' },
580
+ game: { type: 'string', required: false, description: 'Current game directory if the plan moved.' },
581
+ from: { type: 'string', required: true, description: 'Source Google Play track holding the tested release.' },
582
+ to: { type: 'string', required: true, description: 'Target Google Play track; the only track that changes.' },
583
+ 'version-code': { type: 'string', required: false, description: "Version code to promote; defaults to the source track's only completed release." },
584
+ status: { type: 'string', required: false, description: 'Target release status: completed (default), draft or inProgress (staged rollout).' },
585
+ 'user-fraction': { type: 'string', required: false, description: 'Staged rollout fraction for --status inProgress, greater than 0 and less than 1.' },
586
+ 'release-notes': { type: 'string', required: false, description: "Release notes file; defaults to the source release's notes." },
587
+ approve: { type: 'boolean', required: false, description: 'Approve the promotion.' },
588
+ 'confirm-production': { type: 'boolean', required: false, description: 'Confirm a production promotion or staged rollout.' },
589
+ 'dry-run': { type: 'boolean', required: false, description: 'Read both tracks and print the planned update; the edit is discarded.' },
590
+ },
591
+ run: async (ctx) => {
592
+ const game = readOptionalString(ctx.values.game);
593
+ const plan = readNativeDeploymentPlan(readRequiredCliOption(ctx.values.plan, '--plan'), game === undefined ? undefined : path.resolve(game));
594
+ const android = plan.targets.find((entry) => entry.target === 'android');
595
+ if (android === undefined) {
596
+ throw new Error('The deployment plan has no Android target.');
597
+ }
598
+ const dryRun = ctx.values['dry-run'] === true;
599
+ const versionCode = readOptionalString(ctx.values['version-code']);
600
+ if (versionCode !== undefined && !/^[1-9]\d{0,15}$/u.test(versionCode)) {
601
+ throw new Error('--version-code must be a positive integer.');
602
+ }
603
+ const status = readOptionalString(ctx.values.status);
604
+ const userFraction = readOptionalString(ctx.values['user-fraction']);
605
+ const notesFile = readOptionalString(ctx.values['release-notes']);
606
+ const credential = readStoreCredential(plan, 'android', process.env);
607
+ if (credential.target !== 'android') {
608
+ throw new Error('Google Play promote needs the Android submission credential.');
609
+ }
610
+ const result = await promotePlayRelease({
611
+ packageName: android.appId,
612
+ serviceAccountFile: credential.serviceAccountFile,
613
+ fromTrack: readRequiredCliOption(ctx.values.from, '--from'),
614
+ toTrack: readRequiredCliOption(ctx.values.to, '--to'),
615
+ ...(versionCode === undefined ? {} : { versionCode: Number(versionCode) }),
616
+ ...(status === undefined ? {} : { status: status }),
617
+ ...(userFraction === undefined ? {} : { userFraction: parsePlayUserFraction(userFraction) }),
618
+ ...(notesFile === undefined ? {} : { releaseNotes: readPlayReleaseNotesFile(notesFile) }),
619
+ ...(ctx.values['confirm-production'] === true ? { confirmProduction: true } : {}),
620
+ approved: ctx.values.approve === true,
621
+ dryRun,
464
622
  });
465
623
  console.info(JSON.stringify(result, null, 2));
466
624
  },
@@ -482,11 +640,32 @@ const deployCommand = defineI18n({
482
640
  type: 'string', required: false,
483
641
  description: 'Initial version ledger JSON; only for the first release.',
484
642
  },
485
- approve: { type: 'boolean', required: false, description: 'Approve internal test submission.' },
643
+ approve: { type: 'boolean', required: false, description: 'Approve the store submission; needed for any Play track but internal.' },
644
+ track: {
645
+ type: 'string', required: false,
646
+ description: 'Android: Google Play track (internal, alpha, beta, production or a closed-testing track name); defaults to internal.',
647
+ },
648
+ status: {
649
+ type: 'string', required: false,
650
+ description: 'Android: release status draft, completed (default) or inProgress (staged rollout).',
651
+ },
652
+ 'user-fraction': {
653
+ type: 'string', required: false,
654
+ description: 'Android: staged rollout fraction for --status inProgress, greater than 0 and less than 1.',
655
+ },
656
+ 'release-notes': {
657
+ type: 'string', required: false,
658
+ description: 'Android: release notes file, Play Console <en-US>…</en-US> blocks or JSON; 500 characters per language.',
659
+ },
660
+ 'confirm-production': {
661
+ type: 'boolean', required: false,
662
+ description: 'Android: confirm a production release or staged rollout.',
663
+ },
486
664
  },
487
665
  run: async (ctx) => {
488
666
  const game = readOptionalString(ctx.values.game);
489
667
  const plan = readNativeDeploymentPlan(readRequiredCliOption(ctx.values.plan, '--plan'), game === undefined ? undefined : path.resolve(game));
668
+ const androidRelease = await readPlayReleaseOptions(ctx.values, plan.targets.some((entry) => entry.target === 'android'));
490
669
  const infoFile = path.join(packageRoot, 'dist/native-build-info.json');
491
670
  const info = readJsonForCli(infoFile, 'native builder metadata');
492
671
  assertJsonObject(info, 'native builder package metadata');
@@ -512,7 +691,10 @@ const deployCommand = defineI18n({
512
691
  ...(initialLedgerFile === undefined ? {} : {
513
692
  initialLedger: assertPlatformVersionLedger(readJsonForCli(path.resolve(initialLedgerFile), 'initial version ledger')),
514
693
  }),
515
- approved: isDeployApproved(plan, ctx.values.approve === true),
694
+ approved: isNativeDeployApproved(plan, ctx.values.approve === true, androidRelease),
695
+ ...(androidRelease === undefined ? {} : { androidRelease }),
696
+ ...(ctx.values['confirm-production'] === true ? { confirmProduction: true } : {}),
697
+ onDraftAppFallback: reportPlayDraftAppFallback,
516
698
  ...(recordedBuildMarker === undefined ? {} : {
517
699
  onBuildRecorded: (target) => {
518
700
  writeFileSync(recordedBuildMarker, target, { flag: 'wx', mode: 0o600 });
@@ -1628,8 +1810,8 @@ const targetCommand = defineI18n({
1628
1810
  ko: '프로덕션 HTTPS Game Services 기본 URL.',
1629
1811
  },
1630
1812
  providers: {
1631
- en: 'Comma-separated provider IDs. play-billing (Android) and storekit (iOS) install the kit store plugin into the shell and the game, register it with cap sync, and wire an unwired template gateway; notifications installs the Firebase messaging and local notifications plugins for Android and iOS and wires a template gateway; other IDs are recorded only.',
1632
- ko: '쉼표로 구분한 provider ID. play-billing(Android)과 storekit(iOS)은 키트 스토어 플러그인을 셸과 게임에 설치하고 cap sync로 등록하며, 연결되지 않은 템플릿 게이트웨이를 연결합니다. notifications는 Android·iOS용 Firebase 메시징과 로컬 알림 플러그인을 설치하고 템플릿 게이트웨이를 연결합니다. 다른 ID는 기록만 합니다.',
1813
+ en: 'Comma-separated provider IDs. play-billing (Android) and storekit (iOS) install the kit store plugin into the shell and the game, register it with cap sync, and wire an unwired template gateway; notifications installs the Firebase messaging and local notifications plugins for Android and iOS and wires a template gateway; crash-reporting installs the Firebase Crashlytics plugin into the shell and its Gradle plugin into a new Android project; other IDs are recorded only.',
1814
+ ko: '쉼표로 구분한 provider ID. play-billing(Android)과 storekit(iOS)은 키트 스토어 플러그인을 셸과 게임에 설치하고 cap sync로 등록하며, 연결되지 않은 템플릿 게이트웨이를 연결합니다. notifications는 Android·iOS용 Firebase 메시징과 로컬 알림 플러그인을 설치하고 템플릿 게이트웨이를 연결합니다. crash-reporting은 Firebase Crashlytics 플러그인을 셸에, Crashlytics Gradle 플러그인을 새 Android 프로젝트에 설치합니다. 다른 ID는 기록만 합니다.',
1633
1815
  },
1634
1816
  }),
1635
1817
  args: {
@@ -1677,7 +1859,7 @@ const targetCommand = defineI18n({
1677
1859
  providers: {
1678
1860
  type: 'string',
1679
1861
  required: false,
1680
- description: 'Comma-separated provider IDs. play-billing (Android) and storekit (iOS) install the kit store plugin into the shell and the game, register it with cap sync, and wire an unwired template gateway; notifications installs the Firebase messaging and local notifications plugins for Android and iOS and wires a template gateway; other IDs are recorded only.',
1862
+ description: 'Comma-separated provider IDs. play-billing (Android) and storekit (iOS) install the kit store plugin into the shell and the game, register it with cap sync, and wire an unwired template gateway; notifications installs the Firebase messaging and local notifications plugins for Android and iOS and wires a template gateway; crash-reporting installs the Firebase Crashlytics plugin into the shell and its Gradle plugin into a new Android project; other IDs are recorded only.',
1681
1863
  },
1682
1864
  },
1683
1865
  run: (ctx) => {
@@ -1729,6 +1911,11 @@ const targetCommand = defineI18n({
1729
1911
  + plan.notificationPlugins.map((plugin) => `${plugin.packageName}@${plugin.version}`).join(', ')
1730
1912
  + ` into the shell for android and ios${dryRun ? '; cap add or cap sync registers them' : ''}.`);
1731
1913
  }
1914
+ if (plan.crashReportingPlugins.length > 0) {
1915
+ console.info(`${dryRun ? 'Would install' : 'Installed'} crash reporting plugin `
1916
+ + plan.crashReportingPlugins.map((plugin) => `${plugin.packageName}@${plugin.version}`).join(', ')
1917
+ + ` into the shell for android and ios${dryRun ? '; cap add or cap sync registers it' : ''}.`);
1918
+ }
1732
1919
  for (const warning of plan.warnings) {
1733
1920
  console.warn(`Warning: ${warning}`);
1734
1921
  }
@@ -4103,8 +4290,28 @@ function renderHostedPwaVerificationMarkdown(verification) {
4103
4290
  '',
4104
4291
  ].join('\n');
4105
4292
  }
4106
- function isDeployApproved(plan, approveFlag) {
4107
- return plan.approval === 'preapproved-internal-test' || approveFlag;
4293
+ /** Google Play release flags; undefined when none is given (the original internal path). */
4294
+ async function readPlayReleaseOptions(values, android) {
4295
+ const track = readOptionalString(values.track);
4296
+ const status = readOptionalString(values.status);
4297
+ const userFraction = readOptionalString(values['user-fraction']);
4298
+ const releaseNotesFile = readOptionalString(values['release-notes']);
4299
+ const mappingFile = readOptionalString(values['mapping-file']);
4300
+ if (track === undefined && status === undefined && userFraction === undefined
4301
+ && releaseNotesFile === undefined && mappingFile === undefined) {
4302
+ return undefined;
4303
+ }
4304
+ if (!android) {
4305
+ throw new Error('--track, --status, --user-fraction, --release-notes and --mapping-file '
4306
+ + 'apply only to an Android (Google Play) target.');
4307
+ }
4308
+ return createPlayReleaseRequest({ track, status, userFraction, releaseNotesFile, mappingFile });
4309
+ }
4310
+ function reportPlayDraftAppFallback(track) {
4311
+ console.warn(`Google Play accepts only draft releases until this app's first release is rolled out, `
4312
+ + `so the ${track} release is committed as a draft. Open that draft release in Play Console `
4313
+ + 'and roll it out once (docs/GOOGLE_PLAY_RELEASES.md#draft-apps); later submissions can use '
4314
+ + 'their normal status.');
4108
4315
  }
4109
4316
  function readRequiredCliOption(value, label) {
4110
4317
  const read = readOptionalString(value);
@@ -4,6 +4,84 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs
4
4
  import { tmpdir } from "node:os";
5
5
  import path from "node:path";
6
6
 
7
+ // packages/cli/src/native-shell-store-policy.ts
8
+ function readPlistTopLevelValue(plist, key) {
9
+ const masked = plist.replace(/<!--[\s\S]*?-->/gu, (comment) => comment.replace(/[^\n]/gu, " "));
10
+ const marker = new RegExp(`<key>\\s*${escapeRegExp(key)}\\s*</key>\\s*`, "u").exec(masked);
11
+ if (marker === null) {
12
+ return void 0;
13
+ }
14
+ const start = marker.index + marker[0].length;
15
+ const open = /^<([A-Za-z]+)\s*(\/?)>/u.exec(masked.slice(start));
16
+ if (open === null) {
17
+ throw new Error(`Info.plist key ${key} has no readable value.`);
18
+ }
19
+ const tag = open[1] ?? "";
20
+ if (open[2] === "/") {
21
+ const end = start + open[0].length;
22
+ return { start, end, xml: plist.slice(start, end) };
23
+ }
24
+ let depth = 0;
25
+ const tags = new RegExp(`<(/?)${tag}\\b[^>]*?(/?)>`, "gu");
26
+ tags.lastIndex = start;
27
+ for (let match = tags.exec(masked); match !== null; match = tags.exec(masked)) {
28
+ if (match[2] === "/") {
29
+ continue;
30
+ }
31
+ depth += match[1] === "/" ? -1 : 1;
32
+ if (depth === 0) {
33
+ const end = match.index + match[0].length;
34
+ return { start, end, xml: plist.slice(start, end) };
35
+ }
36
+ }
37
+ throw new Error(`Info.plist key ${key} has an unclosed value.`);
38
+ }
39
+ function escapeRegExp(value) {
40
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
41
+ }
42
+ var iosPortrait = "UIInterfaceOrientationPortrait";
43
+ var iosPortraitUpsideDown = "UIInterfaceOrientationPortraitUpsideDown";
44
+ var iosLandscape = [
45
+ "UIInterfaceOrientationLandscapeLeft",
46
+ "UIInterfaceOrientationLandscapeRight"
47
+ ];
48
+ var iosIpadOrientations = [
49
+ iosPortrait,
50
+ iosPortraitUpsideDown,
51
+ ...iosLandscape
52
+ ];
53
+ var iosAppPrivacyManifestXml = [
54
+ '<?xml version="1.0" encoding="UTF-8"?>',
55
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
56
+ '<plist version="1.0">',
57
+ "<dict>",
58
+ " <key>NSPrivacyTracking</key>",
59
+ " <false/>",
60
+ " <key>NSPrivacyTrackingDomains</key>",
61
+ " <array/>",
62
+ " <key>NSPrivacyCollectedDataTypes</key>",
63
+ " <array/>",
64
+ " <key>NSPrivacyAccessedAPITypes</key>",
65
+ " <array/>",
66
+ "</dict>",
67
+ "</plist>",
68
+ ""
69
+ ].join("\n");
70
+ var iosPushEntitlementsFile = "App.entitlements";
71
+ var iosPushEntitlementsSetting = `App/${iosPushEntitlementsFile}`;
72
+ var iosPushEntitlementsXml = [
73
+ '<?xml version="1.0" encoding="UTF-8"?>',
74
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
75
+ '<plist version="1.0">',
76
+ "<dict>",
77
+ " <key>aps-environment</key>",
78
+ " <string>development</string>",
79
+ "</dict>",
80
+ "</plist>",
81
+ ""
82
+ ].join("\n");
83
+ var iosPushEntitlementStep = `add the Push Notifications capability to the App target in Xcode, or create ios/App/App/${iosPushEntitlementsFile} with <key>aps-environment</key><string>development</string> and set CODE_SIGN_ENTITLEMENTS = ${iosPushEntitlementsSetting} in the App target's Debug and Release build settings (Xcode signs the value from the provisioning profile, production for the App Store)`;
84
+
7
85
  // tools/target/native-inspection-environment.ts
8
86
  var allowedInspectionEnvironmentNames = [
9
87
  "PATH",
@@ -89,6 +167,16 @@ ${signingInfo.stderr}`;
89
167
  if (signingInfo.status !== 0 || /Signature=adhoc/iu.test(signingText) || !signingText.split(/\r?\n/u).some((line) => line.trim() === expectedTeamLine)) {
90
168
  throw new Error("iOS app is not signed for the expected development team.");
91
169
  }
170
+ if (input.expectedApsEnvironment !== void 0) {
171
+ const entitlements = runner.run("codesign", ["-d", "--entitlements", "-", "--xml", appPath]);
172
+ if (entitlements.status !== 0) {
173
+ throw new Error("iOS app entitlements could not be read.");
174
+ }
175
+ const problem = iosStorePushEntitlementError(readSignedApsEnvironment(entitlements.stdout));
176
+ if (problem !== void 0) {
177
+ throw new Error(problem);
178
+ }
179
+ }
92
180
  const fields = [
93
181
  ["bundleId", "CFBundleIdentifier", input.expectedBundleId],
94
182
  ["marketingVersion", "CFBundleShortVersionString", input.expectedMarketingVersion],
@@ -117,6 +205,35 @@ ${signingInfo.stderr}`;
117
205
  }
118
206
  return { ...observed, teamId: input.expectedTeamId, signed: true };
119
207
  }
208
+ function readSignedApsEnvironment(codesignOutput) {
209
+ const start = codesignOutput.search(/<\?xml|<plist\b/u);
210
+ if (start === -1) {
211
+ return codesignOutput.trim() === "" ? void 0 : null;
212
+ }
213
+ let value;
214
+ try {
215
+ value = readPlistTopLevelValue(codesignOutput.slice(start), "aps-environment")?.xml;
216
+ } catch {
217
+ return null;
218
+ }
219
+ if (value === void 0) {
220
+ return void 0;
221
+ }
222
+ return /^<string>\s*([^<]*?)\s*<\/string>$/u.exec(value)?.[1] ?? null;
223
+ }
224
+ function iosStorePushEntitlementError(apsEnvironment) {
225
+ if (apsEnvironment === "production") {
226
+ return void 0;
227
+ }
228
+ const fix = "Enable Push Notifications on the App ID in the Apple Developer account, regenerate the App Store provisioning profile (MPGD_IOS_PROVISIONING_PROFILE_FILE), and rebuild (docs/FIREBASE_NOTIFICATIONS.md#4-ios).";
229
+ if (apsEnvironment === void 0) {
230
+ return `The store IPA has no aps-environment entitlement although the shell installs @capacitor-firebase/messaging: the provisioning profile that signed it lacks the Push Notifications capability (the App ID has no push, or the profile was generated before it was enabled), so the app never gets an APNs token. ${fix}`;
231
+ }
232
+ if (apsEnvironment === "development") {
233
+ return `The store IPA is signed with aps-environment = development although it is an App Store build: the export used a development provisioning profile, so its APNs token belongs to the sandbox and production pushes never arrive. Export with the App Store distribution profile. ${fix}`;
234
+ }
235
+ return `The store IPA's aps-environment entitlement is ${apsEnvironment === null ? "unreadable" : apsEnvironment}, not production, so production pushes never arrive. ${fix}`;
236
+ }
120
237
  function assertNoDebugServer(file) {
121
238
  const value = JSON.parse(readFileSync(file, "utf8"));
122
239
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
@@ -147,5 +264,7 @@ function runCommand(command, args) {
147
264
  export {
148
265
  inspectSignedIosApplication,
149
266
  inspectSignedIosArchive,
150
- inspectSignedIosIpa
267
+ inspectSignedIosIpa,
268
+ iosStorePushEntitlementError,
269
+ readSignedApsEnvironment
151
270
  };
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitGitSha": "f90c1b77055c4700195f5e393c3c17b6db23f9bc",
2
+ "kitGitSha": "91eca22c2b6fa735925b3d18da6684422f942d15",
3
3
  "kitDirty": false,
4
- "packageVersion": "0.58.0"
4
+ "packageVersion": "0.61.0"
5
5
  }