@mpgd/cli 0.59.0 → 0.61.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.
Files changed (33) hide show
  1. package/dist/capacitor-shell-starter.d.ts +24 -0
  2. package/dist/capacitor-shell-starter.js +100 -8
  3. package/dist/crash-reporting-starter.d.ts +99 -0
  4. package/dist/crash-reporting-starter.js +271 -0
  5. package/dist/index.d.ts +5 -2
  6. package/dist/index.js +221 -14
  7. package/dist/ios-ipa-inspection.js +120 -1
  8. package/dist/native-build-info.json +2 -2
  9. package/dist/native-build-target.js +1617 -1185
  10. package/dist/native-deploy-run.d.ts +6 -0
  11. package/dist/native-deploy-run.js +19 -1
  12. package/dist/native-deploy-submission.d.ts +20 -1
  13. package/dist/native-deploy-submission.js +94 -7
  14. package/dist/native-shell-ios-push-files.d.ts +9 -0
  15. package/dist/native-shell-ios-push-files.js +65 -0
  16. package/dist/native-shell-store-policy.d.ts +109 -0
  17. package/dist/native-shell-store-policy.js +553 -7
  18. package/dist/play-internal-submission.d.ts +40 -4
  19. package/dist/play-internal-submission.js +215 -48
  20. package/dist/play-publisher-port.d.ts +26 -1
  21. package/dist/play-release-options.d.ts +97 -0
  22. package/dist/play-release-options.js +307 -0
  23. package/dist/play-release-promote.d.ts +59 -0
  24. package/dist/play-release-promote.js +235 -0
  25. package/dist/play-release-rollout.d.ts +57 -0
  26. package/dist/play-release-rollout.js +237 -0
  27. package/dist/play-sdk-adapter.js +26 -3
  28. package/dist/release-state.d.ts +6 -0
  29. package/dist/release-state.js +8 -0
  30. package/package.json +17 -17
  31. package/templates/phaser-game/apps/target-ait/package.json +2 -2
  32. package/templates/phaser-game/package.json +1 -1
  33. package/templates/phaser-game/vite.runtime-target-config.ts +4 -0
@@ -1,10 +1,4 @@
1
- /**
2
- * Store-facing settings of the game-owned Capacitor shell: the iOS
3
- * Info.plist keys App Store Connect reads (export compliance, device
4
- * capabilities). The starter writes them into a newly generated project;
5
- * a production build only reports what an existing shell still lacks, so
6
- * game-owned native files are never rewritten behind the game's back.
7
- */
1
+ import { notificationsMessagingPlugin } from './notifications-starter.js';
8
2
  /**
9
3
  * Adds the iOS keys every kit shell should ship with, keeping any value the
10
4
  * game already set:
@@ -814,3 +808,555 @@ export function inspectIosInfoPlistAds(plist, usesAds) {
814
808
  }
815
809
  return issues;
816
810
  }
811
+ // iOS push (`--providers notifications`).
812
+ /** The entitlements file of a kit shell's App target, in the App group. */
813
+ export const iosPushEntitlementsFile = 'App.entitlements';
814
+ /** Its `CODE_SIGN_ENTITLEMENTS` value, relative to the Xcode project directory (`ios/App`). */
815
+ export const iosPushEntitlementsSetting = `App/${iosPushEntitlementsFile}`;
816
+ /**
817
+ * The Push Notifications entitlement the Xcode capability writes. Apple:
818
+ * "Xcode sets the value of the entitlement based on your app's current
819
+ * provisioning profile", so `development` here signs as `production` with
820
+ * an App Store profile
821
+ * (https://developer.apple.com/documentation/bundleresources/entitlements/aps-environment).
822
+ */
823
+ export const iosPushEntitlementsXml = [
824
+ '<?xml version="1.0" encoding="UTF-8"?>',
825
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
826
+ '<plist version="1.0">',
827
+ '<dict>',
828
+ '\t<key>aps-environment</key>',
829
+ '\t<string>development</string>',
830
+ '</dict>',
831
+ '</plist>',
832
+ '',
833
+ ].join('\n');
834
+ /**
835
+ * The AppDelegate methods `@capacitor-firebase/messaging` documents for iOS:
836
+ * the plugin observes `.capacitorDidRegisterForRemoteNotifications` and hands
837
+ * the APNs token to Firebase, so without them it never gets an FCM token.
838
+ * The plugin README's third method (`didReceiveRemoteNotification`) is left
839
+ * out: the kit sends no silent pushes, and plugin 8.5.2 never calls the
840
+ * completion handler it is given.
841
+ */
842
+ export const iosAppDelegatePushForwardingSwift = [
843
+ ' // @capacitor-firebase/messaging reads the APNs token from these Capacitor',
844
+ ' // notifications (added by mpgd target init capacitor --providers notifications).',
845
+ ' func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {',
846
+ ' NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)',
847
+ ' }',
848
+ '',
849
+ ' func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {',
850
+ ' NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)',
851
+ ' }',
852
+ ];
853
+ /** The call the scene delegate makes right after Capacitor's scene proxy (reference shell, #407). */
854
+ const iosSceneDelegateColdStartCall = [
855
+ '// A killed-app tap is only in connectionOptions.notificationResponse.',
856
+ "// Capacitor 8.5's scene proxy replays URL contexts on capacitorViewDidAppear",
857
+ '// and does not read this, and @capacitor/ios 8.5.2 never does either.',
858
+ 'forwardColdStartNotification(connectionOptions.notificationResponse)',
859
+ ];
860
+ /** The scene delegate methods that replay a cold-start tap (reference shell, #407). */
861
+ export const iosSceneDelegateColdStartSwift = [
862
+ ' /// Replays a cold-start notification response once plugins have registered',
863
+ " /// with Capacitor's notification router. `load()` runs before",
864
+ ' /// `capacitorViewDidAppear`, which is how the scene proxy replays URL contexts.',
865
+ ' private func forwardColdStartNotification(_ response: UNNotificationResponse?) {',
866
+ ' guard let response else { return }',
867
+ ' var token: NSObjectProtocol?',
868
+ ' token = NotificationCenter.default.addObserver(',
869
+ ' forName: .capacitorViewDidAppear,',
870
+ ' object: nil,',
871
+ ' queue: .main',
872
+ ' ) { _ in',
873
+ ' if let token {',
874
+ ' NotificationCenter.default.removeObserver(token)',
875
+ ' }',
876
+ ' self.deliverColdStartNotification(response)',
877
+ ' }',
878
+ ' }',
879
+ '',
880
+ ' private func deliverColdStartNotification(_ response: UNNotificationResponse) {',
881
+ ' let center = UNUserNotificationCenter.current()',
882
+ ' if let delegate = center.delegate {',
883
+ ' delegate.userNotificationCenter?(center, didReceive: response, withCompletionHandler: {})',
884
+ ' return',
885
+ ' }',
886
+ ' guard let controller = window?.rootViewController as? CAPBridgeViewController,',
887
+ ' let bridge = controller.bridge else {',
888
+ ' return',
889
+ ' }',
890
+ ' bridge.notificationRouter.userNotificationCenter(',
891
+ ' center,',
892
+ ' didReceive: response,',
893
+ ' withCompletionHandler: {}',
894
+ ' )',
895
+ ' }',
896
+ ];
897
+ /** Whether the shell's package.json installs the Firebase messaging plugin (`--providers notifications`). */
898
+ export function shellUsesFirebaseMessaging(shellPackage) {
899
+ if (!isRecord(shellPackage)) {
900
+ return false;
901
+ }
902
+ return [shellPackage.dependencies, shellPackage.devDependencies].some((dependencies) => isRecord(dependencies) && dependencies[notificationsMessagingPlugin] !== undefined);
903
+ }
904
+ /**
905
+ * Swift source with comments and string literals blanked to spaces, so
906
+ * offsets stay the same and only code is matched. Nested block comments,
907
+ * multi-line strings and raw strings (`#"..."#`) are handled; an
908
+ * interpolation's code inside a string is blanked with it, and a
909
+ * single-line string that runs into a newline ends there, so one the
910
+ * scanner misreads cannot blank the rest of the file.
911
+ */
912
+ export function stripSwiftCommentsAndStrings(source) {
913
+ let output = '';
914
+ let commentDepth = 0;
915
+ let lineComment = false;
916
+ /** The open string's closing delimiter, and whether `\` escapes in it. */
917
+ let closing;
918
+ for (let index = 0; index < source.length; index += 1) {
919
+ const character = source[index] ?? '';
920
+ const next = source[index + 1] ?? '';
921
+ const blank = character === '\n' ? '\n' : ' ';
922
+ if (lineComment) {
923
+ lineComment = character !== '\n';
924
+ output += blank;
925
+ }
926
+ else if (commentDepth > 0) {
927
+ if (character === '/' && next === '*') {
928
+ commentDepth += 1;
929
+ output += ' ';
930
+ index += 1;
931
+ }
932
+ else if (character === '*' && next === '/') {
933
+ commentDepth -= 1;
934
+ output += ' ';
935
+ index += 1;
936
+ }
937
+ else {
938
+ output += blank;
939
+ }
940
+ }
941
+ else if (closing !== undefined) {
942
+ if (!closing.raw && character === '\\' && next !== '' && next !== '\n') {
943
+ output += ' ';
944
+ index += 1;
945
+ }
946
+ else if (source.startsWith(closing.text, index)) {
947
+ output += ' '.repeat(closing.text.length);
948
+ index += closing.text.length - 1;
949
+ closing = undefined;
950
+ }
951
+ else {
952
+ if (character === '\n' && !closing.multiline) {
953
+ closing = undefined;
954
+ }
955
+ output += blank;
956
+ }
957
+ }
958
+ else if (character === '/' && next === '/') {
959
+ lineComment = true;
960
+ output += ' ';
961
+ index += 1;
962
+ }
963
+ else if (character === '/' && next === '*') {
964
+ commentDepth = 1;
965
+ output += ' ';
966
+ index += 1;
967
+ }
968
+ else {
969
+ const opener = /^(#*)("""|")/u.exec(source.slice(index, index + 64));
970
+ if (opener === null || (opener[1] === '' && character !== '"')) {
971
+ output += character;
972
+ continue;
973
+ }
974
+ const hashes = opener[1] ?? '';
975
+ const quote = opener[2] ?? '"';
976
+ closing = { text: `${quote}${hashes}`, multiline: quote === '"""', raw: hashes !== '' };
977
+ output += ' '.repeat(opener[0].length);
978
+ index += opener[0].length - 1;
979
+ }
980
+ }
981
+ return output;
982
+ }
983
+ /**
984
+ * The offset of the `}` that closes the top-level `class <name>` whose
985
+ * inheritance clause names `conformance`, in source whose comments and
986
+ * strings are blanked, or undefined.
987
+ */
988
+ function findSwiftClassClose(code, name, conformance) {
989
+ const declaration = new RegExp(`\\bclass\\s+${name}\\b([^{};]*)\\{`, 'gu');
990
+ for (const match of code.matchAll(declaration)) {
991
+ const open = (match.index ?? 0) + match[0].length - 1;
992
+ const before = code.slice(0, match.index ?? 0);
993
+ const depthBefore = (before.match(/\{/gu)?.length ?? 0) - (before.match(/\}/gu)?.length ?? 0);
994
+ if (depthBefore !== 0 || !new RegExp(`\\b${conformance}\\b`, 'u').test(match[1] ?? '')) {
995
+ continue;
996
+ }
997
+ let depth = 0;
998
+ for (let index = open; index < code.length; index += 1) {
999
+ if (code[index] === '{') {
1000
+ depth += 1;
1001
+ }
1002
+ else if (code[index] === '}') {
1003
+ depth -= 1;
1004
+ if (depth === 0) {
1005
+ return index;
1006
+ }
1007
+ }
1008
+ }
1009
+ return undefined;
1010
+ }
1011
+ return undefined;
1012
+ }
1013
+ /** `lines` inserted before the class's closing brace, after one blank line. */
1014
+ function insertBeforeSwiftClassClose(source, close, lines) {
1015
+ const newline = source.includes('\r\n') ? '\r\n' : '\n';
1016
+ const head = source.slice(0, close).replace(/\s*$/u, '');
1017
+ return `${head}${newline}${newline}${lines.join(newline)}${newline}${source.slice(close)}`;
1018
+ }
1019
+ /** The body of each `func` whose name or labels include `label`, in blanked source. */
1020
+ function swiftFunctionBodies(code, label) {
1021
+ const bodies = [];
1022
+ for (const match of code.matchAll(new RegExp(`\\bfunc\\b[^{}]*\\b${label}\\b[^{}]*\\{`, 'gu'))) {
1023
+ const open = (match.index ?? 0) + match[0].length - 1;
1024
+ let depth = 0;
1025
+ for (let index = open; index < code.length; index += 1) {
1026
+ if (code[index] === '{') {
1027
+ depth += 1;
1028
+ }
1029
+ else if (code[index] === '}') {
1030
+ depth -= 1;
1031
+ if (depth === 0) {
1032
+ bodies.push(code.slice(open + 1, index));
1033
+ break;
1034
+ }
1035
+ }
1036
+ }
1037
+ }
1038
+ return bodies;
1039
+ }
1040
+ /**
1041
+ * Whether a Swift source's `didRegisterForRemoteNotificationsWithDeviceToken`
1042
+ * hands the APNs token on: it posts `.capacitorDidRegisterForRemoteNotifications`
1043
+ * (what the messaging plugin observes) or sets Firebase's `apnsToken` itself.
1044
+ */
1045
+ export function swiftForwardsPushToken(source) {
1046
+ return swiftFunctionBodies(stripSwiftCommentsAndStrings(source), 'didRegisterForRemoteNotificationsWithDeviceToken').some((body) => /\bcapacitorDidRegisterForRemoteNotifications\b|\.\s*apnsToken\s*=/u.test(body));
1047
+ }
1048
+ /** Whether a Swift source declares a scene delegate class. */
1049
+ export function swiftDeclaresSceneDelegate(source) {
1050
+ return /\bclass\s+\w+\s*:[^{]*\bUIWindowSceneDelegate\b/u.test(stripSwiftCommentsAndStrings(source));
1051
+ }
1052
+ /** Whether a Swift source forwards the scene's cold-start notification response. */
1053
+ export function swiftForwardsColdStartNotification(source) {
1054
+ return /\.\s*notificationResponse\b/u.test(stripSwiftCommentsAndStrings(source));
1055
+ }
1056
+ /**
1057
+ * Adds the APNs registration forwarding (`iosAppDelegatePushForwardingSwift`)
1058
+ * to the `AppDelegate` class. An AppDelegate that already forwards the token
1059
+ * is returned unchanged; undefined when the class is not found or already
1060
+ * implements a registration callback without the forwarding, which is left
1061
+ * to the game.
1062
+ */
1063
+ export function applyIosAppDelegatePushForwarding(source) {
1064
+ if (swiftForwardsPushToken(source)) {
1065
+ return source;
1066
+ }
1067
+ const code = stripSwiftCommentsAndStrings(source);
1068
+ if (/\bdidRegisterForRemoteNotificationsWithDeviceToken\b|\bdidFailToRegisterForRemoteNotificationsWithError\b/u
1069
+ .test(code)) {
1070
+ return undefined;
1071
+ }
1072
+ const close = findSwiftClassClose(code, 'AppDelegate', 'UIApplicationDelegate');
1073
+ return close === undefined
1074
+ ? undefined
1075
+ : insertBeforeSwiftClassClose(source, close, iosAppDelegatePushForwardingSwift);
1076
+ }
1077
+ /**
1078
+ * Adds the cold-start notification forwarding of the kit reference shell
1079
+ * (#407) to Capacitor's `SceneDelegate`: `import UserNotifications`, the
1080
+ * call after `SceneDelegateProxy.shared.scene(_:willConnectTo:options:)`,
1081
+ * and `iosSceneDelegateColdStartSwift`. A scene delegate that already reads
1082
+ * `notificationResponse` is returned unchanged; undefined when it does not
1083
+ * have Capacitor's layout, which is left to the game.
1084
+ */
1085
+ export function applyIosSceneDelegateColdStartForwarding(source) {
1086
+ if (swiftForwardsColdStartNotification(source)) {
1087
+ return source;
1088
+ }
1089
+ const code = stripSwiftCommentsAndStrings(source);
1090
+ const proxyCalls = [...code.matchAll(/SceneDelegateProxy\s*\.\s*shared\s*\.\s*scene\s*\(\s*scene\s*,\s*willConnectTo\s*:\s*session\s*,\s*options\s*:\s*connectionOptions\s*\)/gu)];
1091
+ const capacitorImport = /^import[ \t]+Capacitor[ \t]*(?=\r?$)/mu.exec(code);
1092
+ if (proxyCalls.length !== 1 || capacitorImport === null
1093
+ || /\bforwardColdStartNotification\b|\bdeliverColdStartNotification\b/u.test(code)) {
1094
+ return undefined;
1095
+ }
1096
+ const close = findSwiftClassClose(code, 'SceneDelegate', 'UIWindowSceneDelegate');
1097
+ const call = proxyCalls[0];
1098
+ if (close === undefined || call?.index === undefined || call.index > close) {
1099
+ return undefined;
1100
+ }
1101
+ const newline = source.includes('\r\n') ? '\r\n' : '\n';
1102
+ const lineStart = source.lastIndexOf('\n', call.index) + 1;
1103
+ const indentation = /^[ \t]*/u.exec(source.slice(lineStart))?.[0] ?? '';
1104
+ const lineEnd = code.indexOf('\n', call.index);
1105
+ if (lineEnd === -1 || lineEnd > close || code.slice(call.index + call[0].length, lineEnd).trim() !== '') {
1106
+ return undefined;
1107
+ }
1108
+ const callEnd = source[lineEnd - 1] === '\r' ? lineEnd - 1 : lineEnd;
1109
+ const withMethods = insertBeforeSwiftClassClose(source, close, iosSceneDelegateColdStartSwift);
1110
+ const withCall = `${withMethods.slice(0, callEnd)}${iosSceneDelegateColdStartCall.map((line) => `${newline}${indentation}${line}`).join('')}${withMethods.slice(callEnd)}`;
1111
+ if (/^import[ \t]+UserNotifications[ \t]*(?=\r?$)/mu.test(code)) {
1112
+ return withCall;
1113
+ }
1114
+ const importEnd = capacitorImport.index + capacitorImport[0].length;
1115
+ return `${withCall.slice(0, importEnd)}${newline}import UserNotifications${withCall.slice(importEnd)}`;
1116
+ }
1117
+ /** The App target's Release `CODE_SIGN_ENTITLEMENTS`, without quotes, or undefined. */
1118
+ export function readIosAppReleaseEntitlements(project) {
1119
+ for (const id of readAppTargetConfigurationIds(project)) {
1120
+ const settings = locatePbxBuildSettings(project, id);
1121
+ if (settings.name === 'Release') {
1122
+ return readPbxSetting(project.slice(settings.start, settings.end), 'CODE_SIGN_ENTITLEMENTS');
1123
+ }
1124
+ }
1125
+ return undefined;
1126
+ }
1127
+ function readPbxSetting(settings, name) {
1128
+ const value = new RegExp(`(?:^|[\\s;{])${name}\\s*=\\s*([^;]+);`, 'u').exec(settings)?.[1];
1129
+ return value?.trim().replace(/^"(.*)"$/u, '$1');
1130
+ }
1131
+ /**
1132
+ * The entitlements path a `CODE_SIGN_ENTITLEMENTS` value names, relative to
1133
+ * the Xcode project directory, or undefined when it uses another build
1134
+ * setting the kit cannot resolve.
1135
+ */
1136
+ export function resolveIosEntitlementsSetting(value) {
1137
+ const relative = value.replace(/^\$[({](?:SRCROOT|PROJECT_DIR)[)}]\//u, '');
1138
+ return relative.includes('$') || relative === '' ? undefined : relative;
1139
+ }
1140
+ /**
1141
+ * Adds `App.entitlements` to the App group of a Capacitor-generated project
1142
+ * and sets `CODE_SIGN_ENTITLEMENTS` in every App target build configuration.
1143
+ * The file is not bundled (no build phase). A project that already points
1144
+ * there is returned unchanged; undefined when the project has another layout
1145
+ * or another entitlements file, which is left to the game.
1146
+ */
1147
+ export function applyIosProjectPushEntitlements(project) {
1148
+ try {
1149
+ return addIosProjectPushEntitlements(project);
1150
+ }
1151
+ catch {
1152
+ // An Xcode project this cannot read is left to the game, like any other layout.
1153
+ return undefined;
1154
+ }
1155
+ }
1156
+ function addIosProjectPushEntitlements(project) {
1157
+ const configurationIds = readAppTargetConfigurationIds(project);
1158
+ if (/\bCODE_SIGN_ENTITLEMENTS\s*\[/u.test(project)) {
1159
+ // A per-SDK entitlements setting would override the one this adds.
1160
+ return undefined;
1161
+ }
1162
+ const group = findAppGroup(project);
1163
+ if (group === undefined || !project.includes('/* End PBXFileReference section */')) {
1164
+ return undefined;
1165
+ }
1166
+ for (const id of configurationIds) {
1167
+ const settings = locatePbxBuildSettings(project, id);
1168
+ const current = readPbxSetting(project.slice(settings.start, settings.end), 'CODE_SIGN_ENTITLEMENTS');
1169
+ if (current !== undefined && resolveIosEntitlementsSetting(current) !== iosPushEntitlementsSetting) {
1170
+ return undefined;
1171
+ }
1172
+ }
1173
+ let next = project;
1174
+ if (!group.text.includes(`/* ${iosPushEntitlementsFile} */`)) {
1175
+ const fileId = uniquePbxId(project, `${iosPushEntitlementsFile}:file`);
1176
+ const edits = [
1177
+ {
1178
+ at: project.indexOf('/* End PBXFileReference section */'),
1179
+ text: `\t\t${fileId} /* ${iosPushEntitlementsFile} */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ${iosPushEntitlementsFile}; sourceTree = "<group>"; };\n`,
1180
+ },
1181
+ { at: group.childrenEnd, text: `\t${fileId} /* ${iosPushEntitlementsFile} */,\n\t\t\t` },
1182
+ ].sort((left, right) => right.at - left.at);
1183
+ for (const edit of edits) {
1184
+ next = `${next.slice(0, edit.at)}${edit.text}${next.slice(edit.at)}`;
1185
+ }
1186
+ }
1187
+ for (const id of configurationIds) {
1188
+ const settings = locatePbxBuildSettings(next, id);
1189
+ const body = next.slice(settings.start, settings.end);
1190
+ if (readPbxSetting(body, 'CODE_SIGN_ENTITLEMENTS') === undefined) {
1191
+ next = `${next.slice(0, settings.start)}\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = ${iosPushEntitlementsSetting};${body}${next.slice(settings.end)}`;
1192
+ }
1193
+ }
1194
+ return next;
1195
+ }
1196
+ export const iosPushEntitlementStep = 'add the Push Notifications capability to the App target in '
1197
+ + `Xcode, or create ios/App/App/${iosPushEntitlementsFile} with <key>aps-environment</key>`
1198
+ + `<string>development</string> and set CODE_SIGN_ENTITLEMENTS = ${iosPushEntitlementsSetting} `
1199
+ + "in the App target's Debug and Release build settings (Xcode signs the value from the "
1200
+ + 'provisioning profile, production for the App Store)';
1201
+ export const iosPushTokenForwardingStep = 'add to the AppDelegate class: func application(_ '
1202
+ + 'application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) '
1203
+ + '{ NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: '
1204
+ + 'deviceToken) } and func application(_ application: UIApplication, '
1205
+ + 'didFailToRegisterForRemoteNotificationsWithError error: Error) { NotificationCenter.default.post('
1206
+ + 'name: .capacitorDidFailToRegisterForRemoteNotifications, object: error) }';
1207
+ export const iosPushColdStartStep = 'copy forwardColdStartNotification and '
1208
+ + 'deliverColdStartNotification from the mpgd-kit reference shell '
1209
+ + '(apps/mobile-capacitor/ios/App/App/SceneDelegate.swift) into SceneDelegate, import '
1210
+ + 'UserNotifications, and call forwardColdStartNotification(connectionOptions.notificationResponse) '
1211
+ + 'right after SceneDelegateProxy.shared.scene(scene, willConnectTo: session, options: connectionOptions)';
1212
+ /**
1213
+ * What a shell with `@capacitor-firebase/messaging` still lacks on iOS: the
1214
+ * `aps-environment` entitlement on the App target's Release configuration,
1215
+ * the AppDelegate's APNs token forwarding, and the scene delegate's
1216
+ * cold-start tap forwarding (for a shell with a scene delegate). Each is an
1217
+ * error: without the first two no push reaches the app, and without the
1218
+ * third a tap on a notification while the app is not running opens it
1219
+ * without the notification's route. An entitlement the kit cannot follow
1220
+ * (`inspectIosPushEntitlement`) is a warning. The forwarding checks are
1221
+ * textual: the token must be handed on inside
1222
+ * `didRegisterForRemoteNotificationsWithDeviceToken`, and some App source
1223
+ * must read `.notificationResponse`.
1224
+ */
1225
+ export function inspectIosPushWiring(input) {
1226
+ const issues = [];
1227
+ const guide = ' (see the mpgd-kit Firebase guide, docs/FIREBASE_NOTIFICATIONS.md#4-ios).';
1228
+ const entitlement = inspectIosPushEntitlement(input, guide);
1229
+ if (entitlement !== undefined) {
1230
+ issues.push(entitlement);
1231
+ }
1232
+ if (!input.swiftSources.some(swiftForwardsPushToken)) {
1233
+ issues.push({
1234
+ level: 'error',
1235
+ message: 'The shell installs @capacitor-firebase/messaging, but no App Swift source forwards '
1236
+ + 'the APNs device token to Capacitor, so the plugin never gets an FCM token: '
1237
+ + `${iosPushTokenForwardingStep}${guide}`,
1238
+ });
1239
+ }
1240
+ // A shell without scenes gets a cold-start tap through the notification
1241
+ // center delegate; only a scene delegate must forward it.
1242
+ if (input.swiftSources.some(swiftDeclaresSceneDelegate)
1243
+ && !input.swiftSources.some(swiftForwardsColdStartNotification)) {
1244
+ issues.push({
1245
+ level: 'error',
1246
+ message: 'The shell installs @capacitor-firebase/messaging, but its iOS SceneDelegate does '
1247
+ + 'not forward connectionOptions.notificationResponse, so a tap on a notification while '
1248
+ + "the app is not running opens the game without the notification's route: "
1249
+ + `${iosPushColdStartStep}${guide}`,
1250
+ });
1251
+ }
1252
+ return issues;
1253
+ }
1254
+ /**
1255
+ * The Release configuration's `aps-environment` entitlement: an error when it
1256
+ * is provably missing, a warning when the shell sets it in a way the kit
1257
+ * cannot follow (an xcconfig, a per-SDK setting, another build setting, a
1258
+ * build-setting value).
1259
+ */
1260
+ function inspectIosPushEntitlement(input, guide) {
1261
+ const unverifiable = (reason) => ({
1262
+ level: 'warning',
1263
+ message: `The kit cannot verify the iOS aps-environment entitlement: ${reason}; make sure the `
1264
+ + `App target's Release signing includes it${guide}`,
1265
+ });
1266
+ let release;
1267
+ try {
1268
+ release = readIosAppReleaseSigning(input.project);
1269
+ }
1270
+ catch {
1271
+ return unverifiable('the Xcode project could not be read');
1272
+ }
1273
+ if (release.conditional) {
1274
+ return unverifiable('CODE_SIGN_ENTITLEMENTS is set per SDK');
1275
+ }
1276
+ if (release.entitlements === undefined && release.baseConfiguration) {
1277
+ return unverifiable('the Release configuration has a base xcconfig that may set CODE_SIGN_ENTITLEMENTS');
1278
+ }
1279
+ const relative = release.entitlements === undefined
1280
+ ? undefined
1281
+ : resolveIosEntitlementsSetting(release.entitlements);
1282
+ if (release.entitlements !== undefined && relative === undefined) {
1283
+ return unverifiable(`CODE_SIGN_ENTITLEMENTS is ${release.entitlements}`);
1284
+ }
1285
+ const entitlements = relative === undefined ? undefined : input.readProjectFile(relative);
1286
+ let value;
1287
+ try {
1288
+ value = entitlements === undefined
1289
+ ? undefined
1290
+ : readPlistTopLevelValue(entitlements, 'aps-environment')?.xml;
1291
+ }
1292
+ catch {
1293
+ value = undefined;
1294
+ }
1295
+ if (value !== undefined && /^<string>\s*(?:development|production)\s*<\/string>$/u.test(value)) {
1296
+ return undefined;
1297
+ }
1298
+ if (value !== undefined && /^<string>[^<]*\$[({]/u.test(value)) {
1299
+ return unverifiable(`aps-environment is ${value}`);
1300
+ }
1301
+ return {
1302
+ level: 'error',
1303
+ message: 'The shell installs @capacitor-firebase/messaging, but the iOS App target has no '
1304
+ + 'aps-environment entitlement in its Release configuration'
1305
+ + `${release.entitlements === undefined ? '' : ` (${release.entitlements})`}, so APNs issues no `
1306
+ + `device token and no push reaches the app: ${iosPushEntitlementStep}${guide}`,
1307
+ };
1308
+ }
1309
+ /** The App target's Release signing settings that decide its entitlements file. */
1310
+ /**
1311
+ * The Release signing settings that decide the App target's entitlements
1312
+ * file: the target's own, else what it inherits from the project-level
1313
+ * Release configuration. `baseConfiguration` is set when an xcconfig may
1314
+ * supply or override the value; the entitlements are then unverifiable.
1315
+ */
1316
+ function readIosAppReleaseSigning(project) {
1317
+ const projectObject = findPbxObject(project, (body) => /\bisa\s*=\s*PBXProject\s*;/u.test(body));
1318
+ const projectListId = /\bbuildConfigurationList\s*=\s*([A-F0-9]+)\b/u.exec(projectObject?.text ?? '')?.[1];
1319
+ const projectList = projectListId === undefined
1320
+ ? undefined
1321
+ : findPbxObjectById(project, projectListId);
1322
+ const levels = [
1323
+ readAppTargetConfigurationIds(project),
1324
+ projectList === undefined
1325
+ ? []
1326
+ : (readPbxList(project, projectList, 'buildConfigurations')?.ids ?? []),
1327
+ ];
1328
+ let baseConfiguration = false;
1329
+ for (const ids of levels) {
1330
+ for (const id of ids) {
1331
+ const settings = locatePbxBuildSettings(project, id);
1332
+ if (settings.name !== 'Release') {
1333
+ continue;
1334
+ }
1335
+ const body = project.slice(settings.start, settings.end);
1336
+ const entitlements = readPbxSetting(body, 'CODE_SIGN_ENTITLEMENTS');
1337
+ const conditional = /\bCODE_SIGN_ENTITLEMENTS\s*\[/u.test(body);
1338
+ if (entitlements !== undefined || conditional) {
1339
+ // A target xcconfig outranks the project-level settings: the value
1340
+ // found there may not be the one Xcode signs with.
1341
+ return baseConfiguration
1342
+ ? { entitlements: undefined, conditional: false, baseConfiguration }
1343
+ : { entitlements, conditional, baseConfiguration };
1344
+ }
1345
+ baseConfiguration ||= /\bbaseConfigurationReference\s*=/u.test(findPbxObjectById(project, id)?.text.replace(body, '') ?? '');
1346
+ }
1347
+ }
1348
+ return { entitlements: undefined, conditional: false, baseConfiguration };
1349
+ }
1350
+ /** The Firebase config file the messaging plugin reads from the app bundle. */
1351
+ export const iosFirebaseConfigFile = 'GoogleService-Info.plist';
1352
+ export function inspectIosFirebaseConfig(project) {
1353
+ if (iosAppBundlesResource(project, iosFirebaseConfigFile)) {
1354
+ return [];
1355
+ }
1356
+ return [{
1357
+ level: 'warning',
1358
+ message: `The shell installs @capacitor-firebase/messaging, but the iOS App target does not bundle `
1359
+ + `${iosFirebaseConfigFile}; the plugin then leaves Firebase unconfigured and push fails closed. `
1360
+ + 'Download it from the Firebase console and add it to the App target in Xcode.',
1361
+ }];
1362
+ }
@@ -1,4 +1,5 @@
1
- import type { PlayPublisher } from './play-publisher-port.js';
1
+ import type { PlayPublisher, PlayTrackRelease } from './play-publisher-port.js';
2
+ import { type PlayNewReleaseStatus, type PlayReleaseRequest } from './play-release-options.js';
2
3
  import type { ImmutableNativeBuildRecord } from './release-state.js';
3
4
  export interface PlayInternalSubmissionInput {
4
5
  readonly record: ImmutableNativeBuildRecord;
@@ -10,23 +11,58 @@ export interface PlayInternalSubmissionInput {
10
11
  readonly resumeEditId?: string;
11
12
  /** Persist this edit ID before any upload begins, for later reconciliation. */
12
13
  readonly onEditCreated?: (editId: string) => Promise<void>;
14
+ /** Track, status, rollout fraction, notes and mapping; defaults to internal/completed. */
15
+ readonly release?: PlayReleaseRequest;
16
+ /** Required for production tracks and staged (inProgress) rollouts. */
17
+ readonly confirmProduction?: boolean;
18
+ /** Called before the kit re-commits a release as a draft for a never-published app. */
19
+ readonly onDraftAppFallback?: (track: string) => void;
13
20
  readonly signal?: AbortSignal;
14
21
  }
15
22
  export interface PlayInternalSubmissionResult {
16
23
  readonly status: 'committed';
17
24
  readonly packageName: string;
18
25
  readonly editId: string;
19
- readonly track: 'internal';
26
+ readonly track: string;
27
+ /** The committed release's status; `draft` after a draft-app fallback. */
28
+ readonly releaseStatus: PlayNewReleaseStatus;
29
+ /** Play refused a non-draft release because the app was never published. */
30
+ readonly draftAppFallback: boolean;
20
31
  readonly versionCode: number;
21
32
  readonly bundleSha256: string;
22
33
  readonly alreadyCommitted: boolean;
23
34
  }
35
+ /** The planned edit of a `--dry-run`: verified locally, no Google Play call made. */
36
+ export interface PlaySubmissionPlan {
37
+ readonly dryRun: true;
38
+ readonly packageName: string;
39
+ readonly track: string;
40
+ readonly versionCode: number;
41
+ readonly bundleSha256: string;
42
+ readonly release: PlayTrackRelease;
43
+ readonly mapping?: {
44
+ readonly file: string;
45
+ readonly sha256: string;
46
+ };
47
+ readonly requiresConfirmProduction: boolean;
48
+ readonly operations: readonly string[];
49
+ }
24
50
  export declare class PlaySubmissionUncertainError extends Error {
25
- readonly stage: 'checkpoint' | 'upload' | 'track-update' | 'validate' | 'commit';
51
+ readonly stage: 'checkpoint' | 'upload' | 'deobfuscation-upload' | 'track-update' | 'validate' | 'commit';
26
52
  readonly editId: string;
27
- constructor(stage: 'checkpoint' | 'upload' | 'track-update' | 'validate' | 'commit', editId: string);
53
+ constructor(stage: 'checkpoint' | 'upload' | 'deobfuscation-upload' | 'track-update' | 'validate' | 'commit', editId: string);
28
54
  }
29
55
  /** Submit only a previously inspected, immutable Android build record. */
30
56
  export declare function submitVerifiedAndroidBundle(input: PlayInternalSubmissionInput): Promise<PlayInternalSubmissionResult>;
57
+ /** Verify the record and artifact, then describe the edit without calling Google Play. */
58
+ export declare function planVerifiedAndroidBundleSubmission(input: Omit<PlayInternalSubmissionInput, 'serviceAccountFile'>): Promise<PlaySubmissionPlan>;
31
59
  /** Internal injection point for credential-free SDK contract tests. */
32
60
  export declare function submitVerifiedAndroidBundleWithPublisher(input: PlayInternalSubmissionInput, publisher: PlayPublisher, requestRootUrl?: string): Promise<PlayInternalSubmissionResult>;
61
+ /**
62
+ * The release already on the track matches what this request would create:
63
+ * the rollout fraction (a draft-app fallback has none) and, when given, the
64
+ * release notes in any order.
65
+ */
66
+ export declare function matchesRequestedRelease(observed: PlayTrackRelease, request: PlayReleaseRequest, observedStatus: PlayNewReleaseStatus): boolean;
67
+ export declare function isHttpStatus(error: unknown, status: number): boolean;
68
+ export declare function isDefinitiveHttpError(error: unknown): boolean;