@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.
- package/dist/app-store-iap-asc.d.ts +23 -0
- package/dist/app-store-iap-asc.js +326 -0
- package/dist/app-store-iap-port.d.ts +125 -0
- package/dist/app-store-iap-port.js +14 -0
- package/dist/capacitor-shell-starter.js +18 -1
- package/dist/crash-reporting-starter.d.ts +5 -3
- package/dist/crash-reporting-starter.js +10 -7
- package/dist/crash-symbols-upload.d.ts +67 -0
- package/dist/crash-symbols-upload.js +232 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +143 -2
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-symbols.d.ts +95 -0
- package/dist/native-build-symbols.js +261 -0
- package/dist/native-build-target.js +1044 -767
- package/dist/native-deploy-run.d.ts +2 -0
- package/dist/native-deploy-run.js +60 -10
- package/dist/native-deploy-submission.d.ts +8 -0
- package/dist/native-deploy-submission.js +50 -8
- package/dist/play-product-port.d.ts +101 -0
- package/dist/play-product-port.js +7 -0
- package/dist/play-sdk-adapter.js +67 -0
- package/dist/pnpm-build-approvals.d.ts +59 -0
- package/dist/pnpm-build-approvals.js +157 -0
- package/dist/release-state.d.ts +69 -0
- package/dist/release-state.js +150 -5
- package/dist/release-workspace.d.ts +3 -0
- package/dist/release-workspace.js +44 -1
- package/dist/store-products-app-store.d.ts +144 -0
- package/dist/store-products-app-store.js +632 -0
- package/dist/store-products-command.d.ts +50 -0
- package/dist/store-products-command.js +182 -0
- package/dist/store-products-play.d.ts +112 -0
- package/dist/store-products-play.js +622 -0
- package/dist/store-products.d.ts +46 -0
- package/dist/store-products.js +124 -0
- package/dist/testflight-submission.d.ts +1 -1
- package/package.json +11 -11
- 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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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 {};
|
package/dist/play-sdk-adapter.js
CHANGED
|
@@ -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
|
+
};
|