@mpgd/cli 0.61.1 → 0.63.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.
Files changed (39) hide show
  1. package/dist/app-store-iap-asc.d.ts +23 -0
  2. package/dist/app-store-iap-asc.js +326 -0
  3. package/dist/app-store-iap-port.d.ts +125 -0
  4. package/dist/app-store-iap-port.js +14 -0
  5. package/dist/capacitor-shell-starter.js +18 -1
  6. package/dist/crash-reporting-starter.d.ts +5 -3
  7. package/dist/crash-reporting-starter.js +10 -7
  8. package/dist/crash-symbols-upload.d.ts +67 -0
  9. package/dist/crash-symbols-upload.js +232 -0
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.js +143 -2
  12. package/dist/native-build-info.json +2 -2
  13. package/dist/native-build-symbols.d.ts +95 -0
  14. package/dist/native-build-symbols.js +261 -0
  15. package/dist/native-build-target.js +1044 -767
  16. package/dist/native-deploy-run.d.ts +2 -0
  17. package/dist/native-deploy-run.js +60 -10
  18. package/dist/native-deploy-submission.d.ts +8 -0
  19. package/dist/native-deploy-submission.js +50 -8
  20. package/dist/play-product-port.d.ts +101 -0
  21. package/dist/play-product-port.js +7 -0
  22. package/dist/play-sdk-adapter.js +67 -0
  23. package/dist/pnpm-build-approvals.d.ts +59 -0
  24. package/dist/pnpm-build-approvals.js +157 -0
  25. package/dist/release-state.d.ts +69 -0
  26. package/dist/release-state.js +150 -5
  27. package/dist/release-workspace.d.ts +3 -0
  28. package/dist/release-workspace.js +44 -1
  29. package/dist/store-products-app-store.d.ts +144 -0
  30. package/dist/store-products-app-store.js +632 -0
  31. package/dist/store-products-command.d.ts +50 -0
  32. package/dist/store-products-command.js +182 -0
  33. package/dist/store-products-play.d.ts +112 -0
  34. package/dist/store-products-play.js +622 -0
  35. package/dist/store-products.d.ts +46 -0
  36. package/dist/store-products.js +124 -0
  37. package/dist/testflight-submission.d.ts +1 -1
  38. package/package.json +11 -11
  39. package/templates/phaser-game/pnpm-workspace.yaml +2 -0
@@ -24,6 +24,8 @@ export interface RunNativeDeploymentInput {
24
24
  readonly signal?: AbortSignal;
25
25
  /** Called only after a newly built target has an immutable, committed build record. */
26
26
  readonly onBuildRecorded?: (target: NativeDeployTarget) => void;
27
+ /** Receives the Crashlytics dSYM upload outcome; defaults to `console.warn`. */
28
+ readonly onCrashSymbolsReport?: (message: string) => void;
27
29
  }
28
30
  /** Reserve once, build only missing targets, then submit the recorded binaries. */
29
31
  export declare function runNativeDeployment(input: RunNativeDeploymentInput): Promise<NativeReleaseStatus>;
@@ -4,10 +4,11 @@ import path from 'node:path';
4
4
  import { doctorNativeDeployment, readNativeDeployCredentialNames, readNativeDeployTargetProfile, } from './deploy-planning.js';
5
5
  import { withAndroidUploadSigningSession } from './android-signing-session.js';
6
6
  import { withIosSigningSession } from './ios-signing-session.js';
7
- import { assertSettledAndroidReleaseOptions, submitRecordedNativeTarget, } from './native-deploy-submission.js';
7
+ import { assertSettledAndroidReleaseOptions, submitRecordedNativeTarget, withRecordedR8Mapping, } from './native-deploy-submission.js';
8
8
  import { isNativeSubmissionSettled, readNativeReleaseStatus, recordNativeReleaseBuild, reserveNativeRelease, } from './release-state.js';
9
9
  import { installPinnedReleaseDependencies, pinNativeDeploymentPlan, runPinnedNativeBuild, withPinnedReleaseWorkspace, } from './release-workspace.js';
10
10
  import { inspectAndroidBundleSigner } from './android-bundle-signer.js';
11
+ import { uploadCrashSymbolsForDeployRun } from './crash-symbols-upload.js';
11
12
  import { assertPlayReleaseConfirmed, assertPlayReleaseRequest, } from './play-release-options.js';
12
13
  const gitAuthEnvironmentNames = ['GITHUB_TOKEN', 'GH_TOKEN', 'GIT_ASKPASS'];
13
14
  /** Reserve once, build only missing targets, then submit the recorded binaries. */
@@ -45,7 +46,7 @@ export async function runNativeDeployment(input) {
45
46
  }
46
47
  }
47
48
  // Refuse changed Play options for a committed release before any build work.
48
- assertSettledAndroidReleaseOptions(existing?.submissions.android, input.androidRelease);
49
+ assertSettledAndroidReleaseOptions(existing?.submissions.android, withRecordedR8Mapping(input.androidRelease, existing?.builds.android, input.plan.gameRoot));
49
50
  const pendingSteps = existing === undefined
50
51
  ? {
51
52
  buildTargets: input.plan.targets.map((entry) => entry.target),
@@ -123,20 +124,35 @@ export async function runNativeDeployment(input) {
123
124
  ...(input.signal === undefined ? {} : { signal: input.signal }),
124
125
  });
125
126
  }
127
+ // A --mapping-file that differs from the mapping recorded with the Android
128
+ // build is refused here, before any target (iOS may come first) is submitted.
129
+ status = await readNativeReleaseStatus({
130
+ gameRoot: input.plan.gameRoot,
131
+ gameId: input.gameId,
132
+ releaseKey: input.releaseKey,
133
+ environment,
134
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
135
+ });
136
+ withRecordedR8Mapping(input.androidRelease, status.builds.android, input.plan.gameRoot);
137
+ let fresh = true;
126
138
  for (const entry of input.plan.targets) {
127
- status = await readNativeReleaseStatus({
128
- gameRoot: input.plan.gameRoot,
129
- gameId: input.gameId,
130
- releaseKey: input.releaseKey,
131
- environment,
132
- ...(input.signal === undefined ? {} : { signal: input.signal }),
133
- });
139
+ // The status read above serves the first target; later ones re-read it.
140
+ if (!fresh) {
141
+ status = await readNativeReleaseStatus({
142
+ gameRoot: input.plan.gameRoot,
143
+ gameId: input.gameId,
144
+ releaseKey: input.releaseKey,
145
+ environment,
146
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
147
+ });
148
+ }
149
+ fresh = false;
134
150
  if (status.builds[entry.target] === undefined) {
135
151
  throw new Error(`${entry.target} has no verified build after the native build stage.`);
136
152
  }
137
153
  const submission = status.submissions[entry.target];
138
154
  if (submission !== undefined && isNativeSubmissionSettled(submission.status)) {
139
- assertSettledAndroidReleaseOptions(submission, input.androidRelease);
155
+ assertSettledAndroidReleaseOptions(submission, withRecordedR8Mapping(input.androidRelease, status.builds.android, input.plan.gameRoot));
140
156
  continue;
141
157
  }
142
158
  await submitRecordedNativeTarget({
@@ -156,6 +172,21 @@ export async function runNativeDeployment(input) {
156
172
  ...(input.signal === undefined ? {} : { signal: input.signal }),
157
173
  });
158
174
  }
175
+ // Crashlytics dSYMs of the recorded iOS build, once, only after every store
176
+ // submission went through (a failed or cancelled run reports its own error
177
+ // at once; the next run or `mpgd deploy symbols` uploads them). A slow upload
178
+ // never delays a submission, and the step never throws: a failure is
179
+ // reported and retryable.
180
+ if (input.plan.targets.some((entry) => entry.target === 'ios')
181
+ && input.signal?.aborted !== true) {
182
+ await uploadCrashSymbolsForDeployRun({
183
+ gameRoot: input.plan.gameRoot,
184
+ gameId: input.gameId,
185
+ releaseKey: input.releaseKey,
186
+ environment,
187
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
188
+ }, input.onCrashSymbolsReport);
189
+ }
159
190
  return readNativeReleaseStatus({
160
191
  gameRoot: input.plan.gameRoot,
161
192
  gameId: input.gameId,
@@ -232,6 +263,23 @@ async function buildAndRecordTarget(input, workspace, target, reserved, environm
232
263
  const manifestLocation = `.mpgd/releases/${input.releaseKey}/${target}-manifest-${sha256(built.releaseManifest)}.json`;
233
264
  const artifactFile = persistImmutableFile(input.plan.gameRoot, artifactLocation, built.artifact);
234
265
  const manifestFile = persistImmutableFile(input.plan.gameRoot, manifestLocation, built.releaseManifest);
266
+ // Crash symbols travel with the artifact (and the workflow's release bundle).
267
+ const persistSymbol = (source, name) => {
268
+ const location = `.mpgd/releases/${input.releaseKey}/${target}-${name.replace('{sha}', sha256(source))}`;
269
+ return { file: persistImmutableFile(input.plan.gameRoot, location, source), location };
270
+ };
271
+ const r8Mapping = built.symbols?.r8Mapping === undefined
272
+ ? undefined
273
+ : persistSymbol(built.symbols.r8Mapping, 'mapping-{sha}.txt');
274
+ const crashlytics = built.symbols?.crashlytics;
275
+ const crashlyticsSymbols = crashlytics === undefined
276
+ ? undefined
277
+ : {
278
+ dsymArchive: persistSymbol(crashlytics.dsymArchive, 'dsyms-{sha}.zip'),
279
+ dsyms: crashlytics.dsyms,
280
+ uploadTool: persistSymbol(crashlytics.uploadTool, 'upload-symbols-{sha}'),
281
+ googleServiceInfo: persistSymbol(crashlytics.googleServiceInfo, 'GoogleService-Info-{sha}.plist'),
282
+ };
235
283
  const configDigest = createHash('sha256').update(JSON.stringify({
236
284
  gameGitSha: workspace.input.gameGitSha,
237
285
  lockfileSha256: workspace.input.lockfileSha256,
@@ -268,6 +316,8 @@ async function buildAndRecordTarget(input, workspace, target, reserved, environm
268
316
  expectedReleaseManifestSha256: sha256(manifestFile),
269
317
  inspectedAppId: planned.appId,
270
318
  ...(target === 'android' ? { inspectedSignerSha256 } : { inspectedTeamId: signerOrTeam }),
319
+ ...(r8Mapping === undefined ? {} : { r8Mapping }),
320
+ ...(crashlyticsSymbols === undefined ? {} : { crashlyticsSymbols }),
271
321
  environment,
272
322
  ...(input.signal === undefined ? {} : { signal: input.signal }),
273
323
  });
@@ -50,6 +50,12 @@ export declare function isNativeDeployApproved(plan: Pick<NativeDeploymentPlan,
50
50
  * Google Play options must fail instead of being skipped as done.
51
51
  */
52
52
  export declare function assertSettledAndroidReleaseOptions(checkpoint: NativeSubmissionCheckpoint | undefined, androidRelease: PlayReleaseRequest | undefined): void;
53
+ /**
54
+ * The Google Play request with the R8 mapping recorded with the build: it is
55
+ * used when no `--mapping-file` is given, and an explicit one must have the
56
+ * same bytes. A build without a recorded mapping keeps the request as it is.
57
+ */
58
+ export declare function withRecordedR8Mapping(request: PlayReleaseRequest | undefined, record: Pick<ImmutableNativeBuildRecord, 'r8Mapping'> | undefined, gameRoot: string): PlayReleaseRequest | undefined;
53
59
  /** Resume exactly one verified build; never rebuild or allocate another version here. */
54
60
  export declare function submitRecordedNativeTarget(input: SubmitRecordedNativeTargetInput): Promise<NativeSubmissionCheckpoint>;
55
61
  /** Verify one recorded Android build and print its Play edit without any store call. */
@@ -57,3 +63,5 @@ export declare function planRecordedAndroidSubmission(input: Omit<SubmitRecorded
57
63
  export declare function submitRecordedNativeTargetWithPorts(input: SubmitRecordedNativeTargetInput, ports: NativeSubmissionPorts): Promise<NativeSubmissionCheckpoint>;
58
64
  /** Validate the workflow-restored manifest against the immutable release-state hash. */
59
65
  export declare function verifyRecordedReleaseManifest(gameRoot: string, record: Pick<ImmutableNativeBuildRecord, 'artifactLocation' | 'releaseManifestSha256' | 'target'>): void;
66
+ /** A recorded game-relative file, resolved without leaving the game directory. */
67
+ export declare function resolveRecordedArtifact(gameRoot: string, recordedLocation: string): string;
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
- import { readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { checkpointNativeSubmission, freshSubmissionLeaseExpiresAt, isNativeSubmissionSettled, readNativeReleaseStatus, reclaimNativeSubmission, releaseNativeSubmissionLease, } from './release-state.js';
5
5
  import { planVerifiedAndroidBundleSubmission, PlaySubmissionUncertainError, submitVerifiedAndroidBundle, } from './play-internal-submission.js';
@@ -33,6 +33,35 @@ export function assertSettledAndroidReleaseOptions(checkpoint, androidRelease) {
33
33
  + 'change it with "mpgd deploy rollout" or submit a new release.');
34
34
  }
35
35
  }
36
+ /**
37
+ * The Google Play request with the R8 mapping recorded with the build: it is
38
+ * used when no `--mapping-file` is given, and an explicit one must have the
39
+ * same bytes. A build without a recorded mapping keeps the request as it is.
40
+ */
41
+ export function withRecordedR8Mapping(request, record, gameRoot) {
42
+ const recorded = record?.r8Mapping;
43
+ if (recorded === undefined) {
44
+ return request;
45
+ }
46
+ if (request?.mappingSha256 !== undefined) {
47
+ if (request.mappingSha256 !== recorded.sha256) {
48
+ throw new Error(`--mapping-file differs from the R8 mapping recorded with this build (${recorded.location}, `
49
+ + `sha256 ${recorded.sha256}); omit it to use the recorded mapping.`);
50
+ }
51
+ return request;
52
+ }
53
+ // A missing file is reported when the submission hashes it; an existing one
54
+ // must not resolve outside the game through a symlink.
55
+ const candidate = path.resolve(gameRoot, recorded.location);
56
+ const mappingFile = existsSync(candidate)
57
+ ? resolveRecordedArtifact(gameRoot, recorded.location)
58
+ : candidate;
59
+ return {
60
+ ...(request ?? defaultPlayReleaseRequest),
61
+ mappingFile,
62
+ mappingSha256: recorded.sha256,
63
+ };
64
+ }
36
65
  /** Resume exactly one verified build; never rebuild or allocate another version here. */
37
66
  export async function submitRecordedNativeTarget(input) {
38
67
  return submitRecordedNativeTargetWithPorts(input, productionPorts);
@@ -43,11 +72,12 @@ export async function planRecordedAndroidSubmission(input, readStatus = readNati
43
72
  if (input.environment?.MPGD_VERIFY_RELEASE_MANIFEST === '1') {
44
73
  verifyRecordedReleaseManifest(input.plan.gameRoot, recorded.record);
45
74
  }
75
+ const release = withRecordedR8Mapping(input.androidRelease, recorded.record, input.plan.gameRoot);
46
76
  return planVerifiedAndroidBundleSubmission({
47
77
  record: recorded.record,
48
78
  aabFile: recorded.artifactFile,
49
79
  packageName: recorded.planned.appId,
50
- ...(input.androidRelease === undefined ? {} : { release: input.androidRelease }),
80
+ ...(release === undefined ? {} : { release }),
51
81
  ...(input.signal === undefined ? {} : { signal: input.signal }),
52
82
  });
53
83
  }
@@ -86,14 +116,13 @@ export async function submitRecordedNativeTargetWithPorts(input, ports) {
86
116
  if (planned === undefined || !input.approved) {
87
117
  throw new Error('Native store submission requires a planned target and explicit approval.');
88
118
  }
89
- const androidRelease = input.androidRelease ?? defaultPlayReleaseRequest;
90
119
  if (target === 'android') {
91
120
  // Refuse an invalid or unconfirmed production/staged request before the
92
121
  // release state is read or a checkpoint is written.
93
- assertPlayReleaseRequest(androidRelease);
94
- assertPlayReleaseConfirmed(androidRelease, input.confirmProduction === true);
122
+ const requested = input.androidRelease ?? defaultPlayReleaseRequest;
123
+ assertPlayReleaseRequest(requested);
124
+ assertPlayReleaseConfirmed(requested, input.confirmProduction === true);
95
125
  }
96
- const releaseDigest = target === 'android' ? playReleaseRequestDigest(androidRelease) : undefined;
97
126
  const stateInput = {
98
127
  gameRoot: input.plan.gameRoot,
99
128
  gameId: input.gameId,
@@ -101,6 +130,12 @@ export async function submitRecordedNativeTargetWithPorts(input, ports) {
101
130
  ...(input.signal === undefined ? {} : { signal: input.signal }),
102
131
  };
103
132
  const { record, artifactFile, status } = await readRecordedSubmission(input, target, ports.readStatus);
133
+ // The build's recorded R8 mapping goes with it, the same way --mapping-file does.
134
+ const effectiveAndroidRelease = target === 'android'
135
+ ? withRecordedR8Mapping(input.androidRelease, record, input.plan.gameRoot)
136
+ : undefined;
137
+ const androidRelease = effectiveAndroidRelease ?? defaultPlayReleaseRequest;
138
+ const releaseDigest = target === 'android' ? playReleaseRequestDigest(androidRelease) : undefined;
104
139
  let checkpoint = status.submissions[target];
105
140
  assertSettledAndroidReleaseOptions(checkpoint, androidRelease);
106
141
  // An attempt that never checkpointed a Play edit changed nothing remotely,
@@ -116,6 +151,12 @@ export async function submitRecordedNativeTargetWithPorts(input, ports) {
116
151
  if (checkpoint !== undefined && isNativeSubmissionSettled(checkpoint.status)) {
117
152
  return checkpoint;
118
153
  }
154
+ if (record.r8Mapping !== undefined && effectiveAndroidRelease?.mappingFile !== undefined
155
+ && !existsSync(effectiveAndroidRelease.mappingFile)) {
156
+ throw new Error(`The R8 mapping recorded with this build is missing: ${record.r8Mapping.location}. Restore `
157
+ + 'the release bundle (.mpgd/releases/<release>) next to the bundle, or pass the same file '
158
+ + 'with --mapping-file.');
159
+ }
119
160
  if (input.environment?.MPGD_VERIFY_RELEASE_MANIFEST === '1') {
120
161
  verifyRecordedReleaseManifest(input.plan.gameRoot, record);
121
162
  }
@@ -202,7 +243,7 @@ export async function submitRecordedNativeTargetWithPorts(input, ports) {
202
243
  onEditCreated: async (editId) => {
203
244
  await save({ status: 'edit-open', remoteEditId: editId });
204
245
  },
205
- ...(input.androidRelease === undefined ? {} : { release: input.androidRelease }),
246
+ ...(effectiveAndroidRelease === undefined ? {} : { release: effectiveAndroidRelease }),
206
247
  ...(input.confirmProduction === undefined ? {} : {
207
248
  confirmProduction: input.confirmProduction,
208
249
  }),
@@ -316,7 +357,8 @@ export function verifyRecordedReleaseManifest(gameRoot, record) {
316
357
  throw new Error('Restored release manifest differs from the immutable build record.');
317
358
  }
318
359
  }
319
- function resolveRecordedArtifact(gameRoot, recordedLocation) {
360
+ /** A recorded game-relative file, resolved without leaving the game directory. */
361
+ export function resolveRecordedArtifact(gameRoot, recordedLocation) {
320
362
  if (path.isAbsolute(recordedLocation)) {
321
363
  throw new Error('Native artifact location must be game-relative.');
322
364
  }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Google Play one-time product port (Android Publisher v3
3
+ * `monetization.onetimeproducts`). The Google SDK lives under
4
+ * adapters/play-publisher; the CLI sees only these shapes.
5
+ * https://developers.google.com/android-publisher/api-ref/rest/v3/monetization.onetimeproducts
6
+ */
7
+ /** `Money`: whole `units` as a decimal string and `nanos` (10^-9 units). */
8
+ export interface PlayMoney {
9
+ readonly currencyCode?: string | null | undefined;
10
+ readonly units?: string | null | undefined;
11
+ readonly nanos?: number | null | undefined;
12
+ }
13
+ export interface PlayOneTimeProductListing {
14
+ readonly languageCode?: string | null | undefined;
15
+ readonly title?: string | null | undefined;
16
+ readonly description?: string | null | undefined;
17
+ }
18
+ export interface PlayRegionalPricingConfig {
19
+ readonly regionCode?: string | null | undefined;
20
+ readonly price?: PlayMoney | undefined;
21
+ /** `AVAILABLE`, `NO_LONGER_AVAILABLE`, `AVAILABLE_IF_RELEASED`, `AVAILABLE_FOR_OFFERS_ONLY`. */
22
+ readonly availability?: string | null | undefined;
23
+ }
24
+ export interface PlayNewRegionsConfig {
25
+ readonly usdPrice?: PlayMoney | undefined;
26
+ readonly eurPrice?: PlayMoney | undefined;
27
+ readonly availability?: string | null | undefined;
28
+ }
29
+ export interface PlayPurchaseOption {
30
+ readonly purchaseOptionId?: string | null | undefined;
31
+ readonly buyOption?: {
32
+ readonly legacyCompatible?: boolean | null | undefined;
33
+ readonly multiQuantityEnabled?: boolean | null | undefined;
34
+ } | undefined;
35
+ /** A rent option; the sync never manages one. */
36
+ readonly rentOption?: Readonly<Record<string, unknown>> | undefined;
37
+ readonly regionalPricingAndAvailabilityConfigs?: readonly PlayRegionalPricingConfig[] | undefined;
38
+ readonly newRegionsConfig?: PlayNewRegionsConfig | undefined;
39
+ /** Output only: `DRAFT`, `ACTIVE`, `INACTIVE` or `INACTIVE_PUBLISHED`. */
40
+ readonly state?: string | null | undefined;
41
+ readonly [field: string]: unknown;
42
+ }
43
+ export interface PlayOneTimeProduct {
44
+ readonly packageName?: string | null | undefined;
45
+ readonly productId?: string | null | undefined;
46
+ readonly listings?: readonly PlayOneTimeProductListing[] | undefined;
47
+ readonly purchaseOptions?: readonly PlayPurchaseOption[] | undefined;
48
+ readonly taxAndComplianceSettings?: {
49
+ readonly productTaxCategoryCode?: string | null | undefined;
50
+ readonly [field: string]: unknown;
51
+ } | undefined;
52
+ /** Output only. */
53
+ readonly regionsVersion?: {
54
+ readonly version?: string | null | undefined;
55
+ } | undefined;
56
+ readonly [field: string]: unknown;
57
+ }
58
+ /** `monetization.convertRegionPrices` response. */
59
+ export interface PlayConvertedPrices {
60
+ readonly convertedRegionPrices?: Readonly<Record<string, {
61
+ readonly regionCode?: string | null | undefined;
62
+ /** Tax inclusive. */
63
+ readonly price?: PlayMoney | undefined;
64
+ readonly taxAmount?: PlayMoney | undefined;
65
+ }>> | null | undefined;
66
+ readonly convertedOtherRegionsPrice?: {
67
+ readonly usdPrice?: PlayMoney | undefined;
68
+ readonly eurPrice?: PlayMoney | undefined;
69
+ } | undefined;
70
+ readonly regionVersion?: {
71
+ readonly version?: string | null | undefined;
72
+ } | undefined;
73
+ }
74
+ export interface PlayProductCatalogPort {
75
+ /** Every one-time product of the app (`onetimeproducts.list`, all pages). */
76
+ listOneTimeProducts(packageName: string, signal?: AbortSignal): Promise<PlayOneTimeProduct[]>;
77
+ /** Read-only price conversion (`monetization.convertRegionPrices`). */
78
+ convertRegionPrices(input: {
79
+ readonly packageName: string;
80
+ readonly price: PlayMoney;
81
+ readonly productTaxCategoryCode?: string;
82
+ readonly signal?: AbortSignal;
83
+ }): Promise<PlayConvertedPrices>;
84
+ /** `onetimeproducts.patch`; `allowMissing` creates the product. */
85
+ patchOneTimeProduct(input: {
86
+ readonly packageName: string;
87
+ readonly productId: string;
88
+ readonly product: PlayOneTimeProduct;
89
+ readonly updateMask: string;
90
+ readonly allowMissing: boolean;
91
+ readonly regionsVersion: string;
92
+ readonly signal?: AbortSignal;
93
+ }): Promise<PlayOneTimeProduct>;
94
+ /** `onetimeproducts.purchaseOptions.batchUpdateStates` with one activate request. */
95
+ activatePurchaseOption(input: {
96
+ readonly packageName: string;
97
+ readonly productId: string;
98
+ readonly purchaseOptionId: string;
99
+ readonly signal?: AbortSignal;
100
+ }): Promise<void>;
101
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Google Play one-time product port (Android Publisher v3
3
+ * `monetization.onetimeproducts`). The Google SDK lives under
4
+ * adapters/play-publisher; the CLI sees only these shapes.
5
+ * https://developers.google.com/android-publisher/api-ref/rest/v3/monetization.onetimeproducts
6
+ */
7
+ export {};
@@ -95,7 +95,74 @@ function wrapPublisher(publisher) {
95
95
  }
96
96
  };
97
97
  }
98
+ function createServiceAccountProductCatalog(serviceAccountFile) {
99
+ const googleAuth = new auth.GoogleAuth({ keyFile: serviceAccountFile, scopes: [scope] });
100
+ return wrapProductCatalog(androidpublisher({ version: "v3", auth: googleAuth }));
101
+ }
102
+ function createMockProductCatalog(accessToken, rootUrl) {
103
+ const oauth = new auth.OAuth2();
104
+ oauth.setCredentials({ access_token: accessToken });
105
+ return wrapProductCatalog(androidpublisher({ version: "v3", auth: oauth, rootUrl }));
106
+ }
107
+ function wrapProductCatalog(publisher) {
108
+ const products = publisher.monetization.onetimeproducts;
109
+ return {
110
+ async listOneTimeProducts(packageName, signal) {
111
+ const all = [];
112
+ let pageToken;
113
+ do {
114
+ const response = await products.list({
115
+ packageName,
116
+ pageSize: 1e3,
117
+ ...pageToken === void 0 ? {} : { pageToken }
118
+ }, { signal });
119
+ all.push(...response.data.oneTimeProducts ?? []);
120
+ const next = response.data.nextPageToken;
121
+ pageToken = typeof next === "string" && next !== "" ? next : void 0;
122
+ } while (pageToken !== void 0);
123
+ return all;
124
+ },
125
+ async convertRegionPrices(input) {
126
+ const response = await publisher.monetization.convertRegionPrices({
127
+ packageName: input.packageName,
128
+ requestBody: {
129
+ price: input.price,
130
+ ...input.productTaxCategoryCode === void 0 ? {} : { productTaxCategoryCode: input.productTaxCategoryCode }
131
+ }
132
+ }, { signal: input.signal });
133
+ return response.data;
134
+ },
135
+ async patchOneTimeProduct(input) {
136
+ const response = await products.patch({
137
+ packageName: input.packageName,
138
+ productId: input.productId,
139
+ allowMissing: input.allowMissing,
140
+ updateMask: input.updateMask,
141
+ "regionsVersion.version": input.regionsVersion,
142
+ requestBody: input.product
143
+ }, { retry: false, signal: input.signal });
144
+ return response.data;
145
+ },
146
+ async activatePurchaseOption(input) {
147
+ await products.purchaseOptions.batchUpdateStates({
148
+ packageName: input.packageName,
149
+ productId: input.productId,
150
+ requestBody: {
151
+ requests: [{
152
+ activatePurchaseOptionRequest: {
153
+ packageName: input.packageName,
154
+ productId: input.productId,
155
+ purchaseOptionId: input.purchaseOptionId
156
+ }
157
+ }]
158
+ }
159
+ }, { retry: false, signal: input.signal });
160
+ }
161
+ };
162
+ }
98
163
  export {
164
+ createMockProductCatalog,
99
165
  createMockPublisher,
166
+ createServiceAccountProductCatalog,
100
167
  createServiceAccountPublisher
101
168
  };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * pnpm build-script decisions for the dependencies a notifications game adds.
3
+ *
4
+ * pnpm 11 replaced `onlyBuiltDependencies` with `allowBuilds` and fails an
5
+ * install (`ERR_PNPM_IGNORED_BUILDS`) while a dependency's build script is
6
+ * neither allowed (`true`) nor denied (`false`), because `strictDepBuilds`
7
+ * defaults to `true` (https://pnpm.io/settings/build). The notifications
8
+ * starter adds `firebase`, whose tree has two such scripts:
9
+ *
10
+ * - `@firebase/util` `postinstall.js` only matters for Firebase App Hosting:
11
+ * it reads `FIREBASE_WEBAPP_CONFIG`, may fetch the web app config from
12
+ * firebase.googleapis.com, and bakes it into `dist/postinstall.*` for
13
+ * `initializeApp()` without options. The published `dist` already ships the
14
+ * placeholder that returns `undefined`, and the kit never initializes the web
15
+ * SDK (it is there so the messaging plugin's web fallback resolves), so the
16
+ * script is denied: the install stays offline and reproducible.
17
+ * - `protobufjs` `scripts/postinstall.js` only prints a warning when the parent
18
+ * package declares protobufjs with another semver operator. The game
19
+ * template already allows it for the Apps in Toss toolchain, so the same
20
+ * decision is kept here.
21
+ *
22
+ * `packages/cli/test/pnpm-build-approvals.test.ts` records every package with
23
+ * an install script in the starter's Firebase tree and fails when the starter's
24
+ * firebase version moves without that list (and a decision here) being redone.
25
+ */
26
+ export declare const notificationsBuildApprovals: Readonly<Record<string, boolean>>;
27
+ export type AllowBuildsEdit = {
28
+ readonly kind: 'covered';
29
+ } | {
30
+ readonly kind: 'edited';
31
+ readonly content: string;
32
+ readonly added: readonly string[];
33
+ } | {
34
+ readonly kind: 'unsupported';
35
+ readonly missing: readonly string[];
36
+ };
37
+ /** The package a matcher names: `name`, `name@range`, `@scope/name@range`, or `name@git-url`. */
38
+ export declare function allowBuildsPackageName(matcher: string): string;
39
+ /**
40
+ * Adds an explicit decision for each package in `approvals` that the
41
+ * `pnpm-workspace.yaml` content does not list yet, keeping every existing
42
+ * entry (a game's own decision wins). Only the block-mapping form the game
43
+ * template writes is edited; another form is reported as unsupported so the
44
+ * caller can print the entries instead of rewriting YAML it cannot parse.
45
+ */
46
+ export declare function addAllowBuilds(content: string, approvals: Readonly<Record<string, boolean>>): AllowBuildsEdit;
47
+ /** The `allowBuilds` block to print when the caller cannot edit the file itself. */
48
+ export declare function allowBuildsSnippet(approvals: Readonly<Record<string, boolean>>): string;
49
+ /**
50
+ * The `pnpm-workspace.yaml` edit a notifications game needs before its
51
+ * install adds `firebase`: the new content when the file can be edited, or a
52
+ * warning with the entries to add when it cannot (no file at the game root,
53
+ * where a parent workspace may govern the install, or a form this edit does
54
+ * not parse).
55
+ */
56
+ export declare function planNotificationsBuildApprovals(workspace: string | undefined): {
57
+ readonly content?: string;
58
+ readonly warning?: string;
59
+ };