@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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AscIapPort } from './app-store-iap-port.js';
|
|
2
|
+
/**
|
|
3
|
+
* The in-app purchase port on the pinned `asc` 5.5.0 CLI, with the same
|
|
4
|
+
* binary pin, isolated environment and API key as the TestFlight
|
|
5
|
+
* submission. Each method is one `asc ... --output json` command; the JSON
|
|
6
|
+
* is App Store Connect's own resource document, which `asc` prints as it
|
|
7
|
+
* received it (`internal/asc/types/resources.go` at the 5.5.0 tag).
|
|
8
|
+
*/
|
|
9
|
+
/** Runs one asc command with `--output json` appended and returns the parsed JSON. */
|
|
10
|
+
export type AscCommandRunner = (args: readonly string[], timeoutMs: number) => Promise<unknown>;
|
|
11
|
+
export interface AscCredential {
|
|
12
|
+
readonly ascBinary: string;
|
|
13
|
+
readonly apiKeyId: string;
|
|
14
|
+
readonly apiIssuerId: string;
|
|
15
|
+
readonly apiPrivateKeyBase64: string;
|
|
16
|
+
}
|
|
17
|
+
/** Copy and verify the pinned binary into a private directory; `close` removes it. */
|
|
18
|
+
export declare function openAscIapSession(credential: AscCredential, signal?: AbortSignal): Promise<{
|
|
19
|
+
readonly port: AscIapPort;
|
|
20
|
+
close(): void;
|
|
21
|
+
}>;
|
|
22
|
+
/** The port over any runner; the tests drive it with recorded asc output. */
|
|
23
|
+
export declare function createAscIapPort(run: AscCommandRunner, today?: () => string): AscIapPort;
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { chmodSync, copyFileSync, mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { ReleaseProcessError, runReleaseProcess } from './deploy-process.js';
|
|
5
|
+
import { sameDecimal } from './store-products.js';
|
|
6
|
+
import { isolatedAscEnvironment, verifyPinnedAscBinary } from './testflight-submission.js';
|
|
7
|
+
const lookupTimeoutMs = 60_000;
|
|
8
|
+
const listTimeoutMs = 3 * 60_000;
|
|
9
|
+
const uploadTimeoutMs = 10 * 60_000;
|
|
10
|
+
const maxJsonBytes = 8 * 1024 * 1024;
|
|
11
|
+
/** asc exit code for an App Store Connect 404 (`cmd/exit_codes.go`, 5.5.0). */
|
|
12
|
+
const ascExitNotFound = 4;
|
|
13
|
+
/** Copy and verify the pinned binary into a private directory; `close` removes it. */
|
|
14
|
+
export async function openAscIapSession(credential, signal) {
|
|
15
|
+
if (!path.isAbsolute(credential.ascBinary)) {
|
|
16
|
+
throw new Error('asc binary must be an absolute file path.');
|
|
17
|
+
}
|
|
18
|
+
const tempRoot = mkdtempSync(path.join(tmpdir(), 'mpgd-asc-iap-'));
|
|
19
|
+
try {
|
|
20
|
+
const binary = path.join(tempRoot, 'asc');
|
|
21
|
+
copyFileSync(credential.ascBinary, binary);
|
|
22
|
+
chmodSync(binary, 0o700);
|
|
23
|
+
await verifyPinnedAscBinary(binary);
|
|
24
|
+
const environment = isolatedAscEnvironment(credential, tempRoot);
|
|
25
|
+
const run = async (args, timeoutMs) => {
|
|
26
|
+
const result = await runReleaseProcess({
|
|
27
|
+
command: binary,
|
|
28
|
+
args: [...args, '--output', 'json'],
|
|
29
|
+
cwd: tempRoot,
|
|
30
|
+
environment,
|
|
31
|
+
timeoutMs,
|
|
32
|
+
maxOutputBytes: maxJsonBytes,
|
|
33
|
+
captureMachineStdout: true,
|
|
34
|
+
secretValues: [credential.apiKeyId, credential.apiIssuerId, credential.apiPrivateKeyBase64],
|
|
35
|
+
signal,
|
|
36
|
+
});
|
|
37
|
+
if (result.truncated || result.machineStdout === undefined) {
|
|
38
|
+
throw new Error('asc JSON output was truncated or unavailable.');
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(result.machineStdout);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new Error('asc returned invalid JSON.');
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
port: createAscIapPort(run),
|
|
49
|
+
close: () => rmSync(tempRoot, { recursive: true, force: true }),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** The port over any runner; the tests drive it with recorded asc output. */
|
|
58
|
+
export function createAscIapPort(run, today = () => new Date().toISOString().slice(0, 10)) {
|
|
59
|
+
const optional = async (args) => {
|
|
60
|
+
try {
|
|
61
|
+
return await run(args, lookupTimeoutMs);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error instanceof ReleaseProcessError && error.exitCode === ascExitNotFound) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
async readAppBundleId(appId) {
|
|
72
|
+
const app = single(await run(['apps', 'view', '--id', appId], lookupTimeoutMs), 'apps view');
|
|
73
|
+
const bundleId = app.attributes.bundleId;
|
|
74
|
+
if (app.id !== appId || typeof bundleId !== 'string') {
|
|
75
|
+
throw new Error('asc apps view returned no bundle ID for the app.');
|
|
76
|
+
}
|
|
77
|
+
return bundleId;
|
|
78
|
+
},
|
|
79
|
+
async listIaps(appId) {
|
|
80
|
+
return list(await run(['iap', 'list', '--app', appId, '--paginate'], listTimeoutMs), 'iap list')
|
|
81
|
+
.map((resource) => ({
|
|
82
|
+
id: resource.id,
|
|
83
|
+
productId: requiredString(resource.attributes.productId, 'iap list productId'),
|
|
84
|
+
name: requiredString(resource.attributes.name, 'iap list name'),
|
|
85
|
+
inAppPurchaseType: requiredString(resource.attributes.inAppPurchaseType, 'iap list type'),
|
|
86
|
+
...optionalString('state', resource.attributes.state),
|
|
87
|
+
...optionalString('reviewNote', resource.attributes.reviewNote),
|
|
88
|
+
...(resource.attributes.familySharable === true ? { familySharable: true } : {}),
|
|
89
|
+
}));
|
|
90
|
+
},
|
|
91
|
+
async listVersions(iapId) {
|
|
92
|
+
return list(await run(['iap', 'versions', 'list', '--iap-id', iapId, '--paginate'], listTimeoutMs), 'iap versions list').map((resource) => ({
|
|
93
|
+
id: resource.id,
|
|
94
|
+
...(typeof resource.attributes.version === 'number' ? { version: resource.attributes.version } : {}),
|
|
95
|
+
...optionalString('state', resource.attributes.state),
|
|
96
|
+
}));
|
|
97
|
+
},
|
|
98
|
+
async listLocalizations(versionId) {
|
|
99
|
+
return list(await run(['iap', 'versions', 'localizations', 'list', '--version-id', versionId, '--paginate'], listTimeoutMs), 'iap versions localizations list').map((resource) => ({
|
|
100
|
+
id: resource.id,
|
|
101
|
+
locale: requiredString(resource.attributes.locale, 'localization locale'),
|
|
102
|
+
name: requiredString(resource.attributes.name, 'localization name'),
|
|
103
|
+
...optionalString('description', resource.attributes.description),
|
|
104
|
+
}));
|
|
105
|
+
},
|
|
106
|
+
async readPriceSchedule(iapId) {
|
|
107
|
+
const response = await optional(['iap', 'pricing', 'schedules', 'view', '--iap-id', iapId, '--include', 'baseTerritory']);
|
|
108
|
+
// No schedule: asc exits 4 (not found), or App Store Connect answers with no resource.
|
|
109
|
+
if (response === undefined || isEmptyResource(response)) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const schedule = single(response, 'iap pricing schedules view');
|
|
113
|
+
const baseTerritory = relationshipId(schedule.relationships, 'baseTerritory');
|
|
114
|
+
const resolved = await run(['iap', 'pricing', 'schedules', 'manual-prices', '--schedule-id', schedule.id, '--resolved'], listTimeoutMs);
|
|
115
|
+
if (!isRecord(resolved) || !Array.isArray(resolved.prices)) {
|
|
116
|
+
throw new Error('asc iap pricing schedules manual-prices returned no prices.');
|
|
117
|
+
}
|
|
118
|
+
const manualPrices = resolved.prices.flatMap((row) => isRecord(row) && row.manual === true && typeof row.territory === 'string'
|
|
119
|
+
&& typeof row.customerPrice === 'string'
|
|
120
|
+
? [{ territory: row.territory, customerPrice: row.customerPrice }]
|
|
121
|
+
: []);
|
|
122
|
+
if (baseTerritory === undefined && manualPrices.length === 0) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
// `--resolved` lists only prices in effect; the raw list also holds future ones.
|
|
126
|
+
const all = list(await run(['iap', 'pricing', 'schedules', 'manual-prices', '--schedule-id', schedule.id, '--paginate'], listTimeoutMs), 'iap pricing schedules manual-prices');
|
|
127
|
+
const now = today();
|
|
128
|
+
// A future start or a future end (a temporary price expiring) is a
|
|
129
|
+
// transition a new open-ended schedule would remove.
|
|
130
|
+
const upcoming = (value) => typeof value === 'string' && value.slice(0, 10) > now;
|
|
131
|
+
const scheduledChanges = all.filter((price) => upcoming(price.attributes.startDate) || upcoming(price.attributes.endDate)).length;
|
|
132
|
+
return {
|
|
133
|
+
id: schedule.id,
|
|
134
|
+
...(baseTerritory === undefined ? {} : { baseTerritory }),
|
|
135
|
+
manualPrices,
|
|
136
|
+
scheduledChanges,
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
async readAvailability(iapId) {
|
|
140
|
+
let response;
|
|
141
|
+
try {
|
|
142
|
+
response = await run(['iap', 'pricing', 'availability', 'view', '--iap-id', iapId], lookupTimeoutMs);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
// asc 5.5.0 reports an unset availability as an error naming it (availability.go).
|
|
146
|
+
if (error instanceof ReleaseProcessError
|
|
147
|
+
&& (error.exitCode === ascExitNotFound || error.output.includes('has no availability configured'))) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
if (isEmptyResource(response)) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
const availability = single(response, 'iap pricing availability view');
|
|
156
|
+
const territories = list(await run(['iap', 'pricing', 'availabilities', 'available-territories', '--id', availability.id, '--paginate'], listTimeoutMs), 'iap pricing availabilities available-territories').map((territory) => territory.id);
|
|
157
|
+
return {
|
|
158
|
+
id: availability.id,
|
|
159
|
+
availableInNewTerritories: availability.attributes.availableInNewTerritories === true,
|
|
160
|
+
territories,
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
async readReviewScreenshot(iapId) {
|
|
164
|
+
const response = await optional(['iap', 'review-screenshots', 'view', '--iap-id', iapId]);
|
|
165
|
+
if (!isRecord(response) || !isRecord(response.data) || typeof response.data.id !== 'string'
|
|
166
|
+
|| response.data.id === '') {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
const attributes = isRecord(response.data.attributes) ? response.data.attributes : {};
|
|
170
|
+
return {
|
|
171
|
+
id: response.data.id,
|
|
172
|
+
...optionalString('fileName', attributes.fileName),
|
|
173
|
+
...(typeof attributes.fileSize === 'number' ? { fileSize: attributes.fileSize } : {}),
|
|
174
|
+
...optionalString('sourceFileChecksum', attributes.sourceFileChecksum),
|
|
175
|
+
...optionalString('deliveryState', isRecord(attributes.assetDeliveryState) ? attributes.assetDeliveryState.state : undefined),
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
async listTerritories() {
|
|
179
|
+
const territories = list(await run(['pricing', 'territories', 'list', '--paginate'], listTimeoutMs), 'pricing territories list').map((resource) => resource.id);
|
|
180
|
+
if (territories.length === 0) {
|
|
181
|
+
throw new Error('asc pricing territories list returned no territories.');
|
|
182
|
+
}
|
|
183
|
+
return territories;
|
|
184
|
+
},
|
|
185
|
+
async createIap(input) {
|
|
186
|
+
const created = single(await run([
|
|
187
|
+
'iap', 'create',
|
|
188
|
+
'--app', input.appId,
|
|
189
|
+
'--type', input.type,
|
|
190
|
+
'--ref-name', input.referenceName,
|
|
191
|
+
'--product-id', input.productId,
|
|
192
|
+
...(input.familySharable ? ['--family-sharable'] : []),
|
|
193
|
+
], lookupTimeoutMs), 'iap create');
|
|
194
|
+
return created.id;
|
|
195
|
+
},
|
|
196
|
+
async updateIap(input) {
|
|
197
|
+
await run([
|
|
198
|
+
'iap', 'update',
|
|
199
|
+
'--id', input.iapId,
|
|
200
|
+
...(input.referenceName === undefined ? [] : ['--ref-name', input.referenceName]),
|
|
201
|
+
...(input.familySharable === true ? ['--family-sharable'] : []),
|
|
202
|
+
], lookupTimeoutMs);
|
|
203
|
+
},
|
|
204
|
+
async createVersion(iapId) {
|
|
205
|
+
return single(await run(['iap', 'versions', 'create', '--iap-id', iapId], lookupTimeoutMs), 'iap versions create').id;
|
|
206
|
+
},
|
|
207
|
+
async createLocalization(input) {
|
|
208
|
+
await run([
|
|
209
|
+
'iap', 'versions', 'localizations', 'create',
|
|
210
|
+
'--version-id', input.versionId,
|
|
211
|
+
'--locale', input.locale,
|
|
212
|
+
'--name', input.name,
|
|
213
|
+
'--description', input.description,
|
|
214
|
+
], lookupTimeoutMs);
|
|
215
|
+
},
|
|
216
|
+
async updateLocalization(input) {
|
|
217
|
+
await run([
|
|
218
|
+
'iap', 'versions', 'localizations', 'update',
|
|
219
|
+
'--localization-id', input.localizationId,
|
|
220
|
+
'--name', input.name,
|
|
221
|
+
'--description', input.description,
|
|
222
|
+
], lookupTimeoutMs);
|
|
223
|
+
},
|
|
224
|
+
async findPricePoint(input) {
|
|
225
|
+
const points = list(await run([
|
|
226
|
+
'iap', 'pricing', 'price-points', 'list',
|
|
227
|
+
'--iap-id', input.iapId,
|
|
228
|
+
'--territory', input.territory,
|
|
229
|
+
'--price', input.customerPrice,
|
|
230
|
+
'--paginate',
|
|
231
|
+
], listTimeoutMs), 'iap pricing price-points list').filter((point) => typeof point.attributes.customerPrice === 'string'
|
|
232
|
+
&& sameDecimal(point.attributes.customerPrice, input.customerPrice));
|
|
233
|
+
const [point] = points;
|
|
234
|
+
if (points.length !== 1 || point === undefined) {
|
|
235
|
+
throw new Error(`App Store Connect has ${points.length} price points of ${input.customerPrice} in ${input.territory}; `
|
|
236
|
+
+ 'use a price that is an App Store price point there.');
|
|
237
|
+
}
|
|
238
|
+
return point.id;
|
|
239
|
+
},
|
|
240
|
+
async createPriceSchedule(input) {
|
|
241
|
+
await run([
|
|
242
|
+
'iap', 'pricing', 'schedules', 'create',
|
|
243
|
+
'--iap-id', input.iapId,
|
|
244
|
+
'--base-territory', input.baseTerritory,
|
|
245
|
+
'--prices', input.pricePointIds.join(','),
|
|
246
|
+
], lookupTimeoutMs);
|
|
247
|
+
},
|
|
248
|
+
async setAvailability(input) {
|
|
249
|
+
await run([
|
|
250
|
+
'iap', 'pricing', 'availability', 'set',
|
|
251
|
+
'--iap-id', input.iapId,
|
|
252
|
+
'--territories', input.territories.join(','),
|
|
253
|
+
...(input.availableInNewTerritories ? ['--available-in-new-territories'] : []),
|
|
254
|
+
], lookupTimeoutMs);
|
|
255
|
+
},
|
|
256
|
+
async deleteReviewScreenshot(screenshotId) {
|
|
257
|
+
await run(['iap', 'review-screenshots', 'delete', '--screenshot-id', screenshotId, '--confirm'], lookupTimeoutMs);
|
|
258
|
+
},
|
|
259
|
+
async uploadReviewScreenshot(input) {
|
|
260
|
+
await run(['iap', 'review-screenshots', 'create', '--iap-id', input.iapId, '--file', input.file], uploadTimeoutMs);
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function single(value, command) {
|
|
265
|
+
const resource = isRecord(value) ? toResource(value.data) : undefined;
|
|
266
|
+
if (resource === undefined) {
|
|
267
|
+
throw new Error(`asc ${command} returned no resource.`);
|
|
268
|
+
}
|
|
269
|
+
return resource;
|
|
270
|
+
}
|
|
271
|
+
function list(value, command) {
|
|
272
|
+
const resources = isRecord(value) && Array.isArray(value.data)
|
|
273
|
+
? value.data.map(toResource)
|
|
274
|
+
: undefined;
|
|
275
|
+
if (resources === undefined || resources.some((resource) => resource === undefined)) {
|
|
276
|
+
throw new Error(`asc ${command} returned no resource list.`);
|
|
277
|
+
}
|
|
278
|
+
return resources;
|
|
279
|
+
}
|
|
280
|
+
function toResource(value) {
|
|
281
|
+
if (!isRecord(value) || typeof value.id !== 'string' || value.id === '') {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
id: value.id,
|
|
286
|
+
attributes: isRecord(value.attributes) ? value.attributes : {},
|
|
287
|
+
...(value.relationships === undefined ? {} : { relationships: value.relationships }),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* App Store Connect's `data: null`, or asc's rendering of it: asc 5.5.0
|
|
292
|
+
* decodes into a non-pointer resource and prints `{ "type": "", "id": "" }`.
|
|
293
|
+
*/
|
|
294
|
+
function isEmptyResource(value) {
|
|
295
|
+
if (!isRecord(value)) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
return value.data === null
|
|
299
|
+
|| (isRecord(value.data) && (value.data.id === undefined || value.data.id === ''));
|
|
300
|
+
}
|
|
301
|
+
function relationshipId(relationships, name) {
|
|
302
|
+
if (!isRecord(relationships)) {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
const relationship = relationships[name];
|
|
306
|
+
if (!isRecord(relationship) || !isRecord(relationship.data)) {
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
return typeof relationship.data.id === 'string' ? relationship.data.id : undefined;
|
|
310
|
+
}
|
|
311
|
+
function requiredString(value, label) {
|
|
312
|
+
if (typeof value !== 'string') {
|
|
313
|
+
throw new Error(`asc output has no ${label}.`);
|
|
314
|
+
}
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
function optionalString(key, value) {
|
|
318
|
+
const result = {};
|
|
319
|
+
if (typeof value === 'string' && value !== '') {
|
|
320
|
+
result[key] = value;
|
|
321
|
+
}
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
function isRecord(value) {
|
|
325
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
326
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Store Connect in-app purchase port. The kit drives App Store Connect
|
|
3
|
+
* through the pinned `asc` CLI (docs/NATIVE_DEPLOY_TOOLING_DECISION.md);
|
|
4
|
+
* `app-store-iap-asc.ts` implements this port with it, and the planner and
|
|
5
|
+
* tests see only these shapes.
|
|
6
|
+
*
|
|
7
|
+
* Resources (App Store Connect API 4.4.1):
|
|
8
|
+
* - In-app purchases v2: https://developer.apple.com/documentation/appstoreconnectapi/in-app-purchases
|
|
9
|
+
* - Versions and their localizations: https://developer.apple.com/documentation/appstoreconnectapi/in-app-purchase-versions
|
|
10
|
+
* - Price schedules: https://developer.apple.com/documentation/appstoreconnectapi/post-v1-inapppurchasepriceschedules
|
|
11
|
+
* - Availability: https://developer.apple.com/documentation/appstoreconnectapi/post-v1-inapppurchaseavailabilities
|
|
12
|
+
* - Review screenshot: https://developer.apple.com/documentation/appstoreconnectapi/post-v1-inapppurchaseappstorereviewscreenshots
|
|
13
|
+
*/
|
|
14
|
+
export type AscIapType = 'CONSUMABLE' | 'NON_CONSUMABLE';
|
|
15
|
+
export interface AscIap {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly productId: string;
|
|
18
|
+
/** Reference name. */
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly inAppPurchaseType: string;
|
|
21
|
+
/** `InAppPurchaseState`, for example `MISSING_METADATA` or `APPROVED`. */
|
|
22
|
+
readonly state?: string | undefined;
|
|
23
|
+
readonly familySharable?: boolean | undefined;
|
|
24
|
+
readonly reviewNote?: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
export interface AscIapVersion {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly version?: number | undefined;
|
|
29
|
+
/** `PREPARE_FOR_SUBMISSION`, `READY_FOR_REVIEW`, `WAITING_FOR_REVIEW`, `APPROVED`, ... */
|
|
30
|
+
readonly state?: string | undefined;
|
|
31
|
+
}
|
|
32
|
+
export interface AscIapLocalization {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly locale: string;
|
|
35
|
+
readonly name: string;
|
|
36
|
+
readonly description?: string | undefined;
|
|
37
|
+
}
|
|
38
|
+
/** The current price schedule: base territory and the manual prices in effect. */
|
|
39
|
+
export interface AscIapPriceSchedule {
|
|
40
|
+
readonly id: string;
|
|
41
|
+
readonly baseTerritory?: string | undefined;
|
|
42
|
+
/** Manual (not equalized) prices in effect now, per alpha-3 territory. */
|
|
43
|
+
readonly manualPrices: readonly {
|
|
44
|
+
readonly territory: string;
|
|
45
|
+
readonly customerPrice: string;
|
|
46
|
+
}[];
|
|
47
|
+
/** Manual prices that start in the future; a new schedule would drop them. */
|
|
48
|
+
readonly scheduledChanges: number;
|
|
49
|
+
}
|
|
50
|
+
export interface AscIapAvailability {
|
|
51
|
+
readonly id: string;
|
|
52
|
+
readonly availableInNewTerritories: boolean;
|
|
53
|
+
/** Alpha-3 territories the in-app purchase is sold in. */
|
|
54
|
+
readonly territories: readonly string[];
|
|
55
|
+
}
|
|
56
|
+
export interface AscIapReviewScreenshot {
|
|
57
|
+
readonly id: string;
|
|
58
|
+
readonly fileName?: string | undefined;
|
|
59
|
+
readonly fileSize?: number | undefined;
|
|
60
|
+
/** MD5 of the uploaded file, as App Store Connect records it. */
|
|
61
|
+
readonly sourceFileChecksum?: string | undefined;
|
|
62
|
+
/** `assetDeliveryState.state`: `AWAITING_UPLOAD`, `UPLOAD_COMPLETE`, `COMPLETE` or `FAILED`. */
|
|
63
|
+
readonly deliveryState?: string | undefined;
|
|
64
|
+
}
|
|
65
|
+
export interface AscIapPort {
|
|
66
|
+
/** The app record's bundle ID (`apps view`). */
|
|
67
|
+
readAppBundleId(appId: string): Promise<string>;
|
|
68
|
+
listIaps(appId: string): Promise<AscIap[]>;
|
|
69
|
+
listVersions(iapId: string): Promise<AscIapVersion[]>;
|
|
70
|
+
listLocalizations(versionId: string): Promise<AscIapLocalization[]>;
|
|
71
|
+
/** `undefined` when the in-app purchase has no price schedule yet. */
|
|
72
|
+
readPriceSchedule(iapId: string): Promise<AscIapPriceSchedule | undefined>;
|
|
73
|
+
/** `undefined` when no availability was set. */
|
|
74
|
+
readAvailability(iapId: string): Promise<AscIapAvailability | undefined>;
|
|
75
|
+
/** `undefined` when no review screenshot was uploaded. */
|
|
76
|
+
readReviewScreenshot(iapId: string): Promise<AscIapReviewScreenshot | undefined>;
|
|
77
|
+
/** Every App Store territory (alpha-3). */
|
|
78
|
+
listTerritories(): Promise<string[]>;
|
|
79
|
+
createIap(input: {
|
|
80
|
+
readonly appId: string;
|
|
81
|
+
readonly type: AscIapType;
|
|
82
|
+
readonly referenceName: string;
|
|
83
|
+
readonly productId: string;
|
|
84
|
+
readonly familySharable: boolean;
|
|
85
|
+
}): Promise<string>;
|
|
86
|
+
updateIap(input: {
|
|
87
|
+
readonly iapId: string;
|
|
88
|
+
readonly referenceName?: string;
|
|
89
|
+
readonly familySharable?: true;
|
|
90
|
+
}): Promise<void>;
|
|
91
|
+
createVersion(iapId: string): Promise<string>;
|
|
92
|
+
createLocalization(input: {
|
|
93
|
+
readonly versionId: string;
|
|
94
|
+
readonly locale: string;
|
|
95
|
+
readonly name: string;
|
|
96
|
+
readonly description: string;
|
|
97
|
+
}): Promise<void>;
|
|
98
|
+
updateLocalization(input: {
|
|
99
|
+
readonly localizationId: string;
|
|
100
|
+
readonly name: string;
|
|
101
|
+
readonly description: string;
|
|
102
|
+
}): Promise<void>;
|
|
103
|
+
/** The price point of `customerPrice` in `territory`; throws unless exactly one matches. */
|
|
104
|
+
findPricePoint(input: {
|
|
105
|
+
readonly iapId: string;
|
|
106
|
+
readonly territory: string;
|
|
107
|
+
readonly customerPrice: string;
|
|
108
|
+
}): Promise<string>;
|
|
109
|
+
/** A new schedule effective now: the base territory's and the manual prices. */
|
|
110
|
+
createPriceSchedule(input: {
|
|
111
|
+
readonly iapId: string;
|
|
112
|
+
readonly baseTerritory: string;
|
|
113
|
+
readonly pricePointIds: readonly string[];
|
|
114
|
+
}): Promise<void>;
|
|
115
|
+
setAvailability(input: {
|
|
116
|
+
readonly iapId: string;
|
|
117
|
+
readonly territories: readonly string[];
|
|
118
|
+
readonly availableInNewTerritories: boolean;
|
|
119
|
+
}): Promise<void>;
|
|
120
|
+
deleteReviewScreenshot(screenshotId: string): Promise<void>;
|
|
121
|
+
uploadReviewScreenshot(input: {
|
|
122
|
+
readonly iapId: string;
|
|
123
|
+
readonly file: string;
|
|
124
|
+
}): Promise<void>;
|
|
125
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Store Connect in-app purchase port. The kit drives App Store Connect
|
|
3
|
+
* through the pinned `asc` CLI (docs/NATIVE_DEPLOY_TOOLING_DECISION.md);
|
|
4
|
+
* `app-store-iap-asc.ts` implements this port with it, and the planner and
|
|
5
|
+
* tests see only these shapes.
|
|
6
|
+
*
|
|
7
|
+
* Resources (App Store Connect API 4.4.1):
|
|
8
|
+
* - In-app purchases v2: https://developer.apple.com/documentation/appstoreconnectapi/in-app-purchases
|
|
9
|
+
* - Versions and their localizations: https://developer.apple.com/documentation/appstoreconnectapi/in-app-purchase-versions
|
|
10
|
+
* - Price schedules: https://developer.apple.com/documentation/appstoreconnectapi/post-v1-inapppurchasepriceschedules
|
|
11
|
+
* - Availability: https://developer.apple.com/documentation/appstoreconnectapi/post-v1-inapppurchaseavailabilities
|
|
12
|
+
* - Review screenshot: https://developer.apple.com/documentation/appstoreconnectapi/post-v1-inapppurchaseappstorereviewscreenshots
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
@@ -11,6 +11,7 @@ import { addIosAppResource, applyAndroidManifestAdId, applyAndroidManifestOrient
|
|
|
11
11
|
import { readIosPushWiringInput } from './native-shell-ios-push-files.js';
|
|
12
12
|
import { isNonPublicServiceHostname } from './production-target-readiness.js';
|
|
13
13
|
import { assertAndroidSettingsAppProject, assertAndroidSettingsNoAppRemap, assertIosReleaseInfoPlistExpansion, assertIosReleasePackagingSettings, assertIosReleaseProductName, assertNativeShellIdentity, hasAndroidDisplayNameResourceOverride, hasAndroidManifestSourceSetOverride, hasAndroidResourceSourceSetOverride, hasGradleIdentityMutation, hasGradleTaskAction, maskGradleStrings, readIosAppTargetId, readIosReleaseInfoPlist, stripGradleComments, } from './native-shell-identity.js';
|
|
14
|
+
import { planNotificationsBuildApprovals } from './pnpm-build-approvals.js';
|
|
14
15
|
const shellPath = 'apps/mobile-capacitor';
|
|
15
16
|
const webPath = `${shellPath}/www`;
|
|
16
17
|
const referenceShellPath = '${MPGD_KIT_PATH}/apps/mobile-capacitor';
|
|
@@ -500,6 +501,17 @@ export function planCapacitorShellStarter(input) {
|
|
|
500
501
|
});
|
|
501
502
|
}
|
|
502
503
|
requestedFiles.push(...gatewayFiles);
|
|
504
|
+
if (notifications !== undefined) {
|
|
505
|
+
// pnpm 11 fails an install on an unreviewed dependency build script, and
|
|
506
|
+
// the firebase package notifications add brings two.
|
|
507
|
+
const approvals = planNotificationsBuildApprovals(readExisting(safeDestination(gameRoot, 'pnpm-workspace.yaml')));
|
|
508
|
+
if (approvals.content !== undefined) {
|
|
509
|
+
requestedFiles.push({ path: 'pnpm-workspace.yaml', content: approvals.content });
|
|
510
|
+
}
|
|
511
|
+
if (approvals.warning !== undefined) {
|
|
512
|
+
warnings.push(approvals.warning);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
503
515
|
// The game bundle imports the plugin from its gateway file, so the game
|
|
504
516
|
// declares it too, at the shell's exact version; the materializer's game
|
|
505
517
|
// install picks it up.
|
|
@@ -668,7 +680,12 @@ export function materializeCapacitorShellStarter(plan, runner = { run: runComman
|
|
|
668
680
|
// workspace globs. Always retry its standalone install: a CLI shim alone
|
|
669
681
|
// does not prove that an interrupted dependency install completed.
|
|
670
682
|
runner.run('pnpm', ['install', '--no-frozen-lockfile'], plan.gameRoot);
|
|
671
|
-
runner.run('pnpm',
|
|
683
|
+
runner.run('pnpm',
|
|
684
|
+
// --ignore-workspace also drops every pnpm-workspace.yaml setting, so no
|
|
685
|
+
// allowBuilds entry can review a dependency build script here and pnpm 11
|
|
686
|
+
// fails on any (firebase brings two when the shell resolves it). The
|
|
687
|
+
// shell's packages need none: Gradle and Xcode build the native code.
|
|
688
|
+
['--dir', shellPath, 'install', '--ignore-workspace', '--ignore-scripts', '--no-frozen-lockfile'], plan.gameRoot);
|
|
672
689
|
for (const platform of plan.nativePlatformsToAdd) {
|
|
673
690
|
const generatedNow = !existsSync(path.join(plan.gameRoot, shellPath, platform));
|
|
674
691
|
if (generatedNow) {
|
|
@@ -14,9 +14,11 @@ export declare const crashlyticsGradlePluginVersion = "3.0.8";
|
|
|
14
14
|
/** The manual Android steps for a shell whose Gradle files the CLI does not rewrite. */
|
|
15
15
|
export declare const crashReportingAndroidSteps: readonly string[];
|
|
16
16
|
/**
|
|
17
|
-
* The iOS
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* The iOS dSYM note: the plugin captures crashes once `GoogleService-Info.plist`
|
|
18
|
+
* is in the App target, and readable native stack traces need the dSYMs
|
|
19
|
+
* uploaded. `mpgd deploy run` records and uploads them for the archives it
|
|
20
|
+
* builds; an archive made another way needs Firebase's run script or
|
|
21
|
+
* `upload-symbols` (docs/CRASH_REPORTING.md#symbols).
|
|
20
22
|
*/
|
|
21
23
|
export declare const crashReportingIosSteps: readonly string[];
|
|
22
24
|
export interface PlannedCrashReporting {
|
|
@@ -31,15 +31,18 @@ export const crashReportingAndroidSteps = Object.freeze([
|
|
|
31
31
|
`In android/app/build.gradle, add apply plugin: '${crashlyticsGradlePluginId}' right after apply plugin: 'com.google.gms.google-services', inside the same google-services.json check.`,
|
|
32
32
|
]);
|
|
33
33
|
/**
|
|
34
|
-
* The iOS
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* The iOS dSYM note: the plugin captures crashes once `GoogleService-Info.plist`
|
|
35
|
+
* is in the App target, and readable native stack traces need the dSYMs
|
|
36
|
+
* uploaded. `mpgd deploy run` records and uploads them for the archives it
|
|
37
|
+
* builds; an archive made another way needs Firebase's run script or
|
|
38
|
+
* `upload-symbols` (docs/CRASH_REPORTING.md#symbols).
|
|
37
39
|
*/
|
|
38
40
|
export const crashReportingIosSteps = Object.freeze([
|
|
39
|
-
'
|
|
40
|
-
+ '
|
|
41
|
-
+ '
|
|
42
|
-
+ '
|
|
41
|
+
'dSYMs: mpgd deploy run uploads them for every App Store build it makes (retry with mpgd '
|
|
42
|
+
+ 'deploy symbols). For an archive made in Xcode or another pipeline, add the Crashlytics run '
|
|
43
|
+
+ 'script build phase ("${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/'
|
|
44
|
+
+ 'Crashlytics/run") as the last phase of the App target, or run upload-symbols after the '
|
|
45
|
+
+ 'archive; see docs/CRASH_REPORTING.md in mpgd-kit.',
|
|
43
46
|
]);
|
|
44
47
|
/**
|
|
45
48
|
* Plan the crash-reporting provider before anything is written. Refused when
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type ReleaseProcessInput, type ReleaseProcessResult } from './deploy-process.js';
|
|
2
|
+
import type { NativeDsymIdentity } from './native-build-symbols.js';
|
|
3
|
+
import { readNativeReleaseStatus, recordCrashSymbolUpload, type CrashSymbolUploadRecord } from './release-state.js';
|
|
4
|
+
export interface CrashSymbolUploadPlan {
|
|
5
|
+
readonly releaseKey: string;
|
|
6
|
+
readonly buildRunId: string;
|
|
7
|
+
readonly dsymArchiveSha256: string;
|
|
8
|
+
readonly dsyms: readonly NativeDsymIdentity[];
|
|
9
|
+
/** `upload-symbols` and its arguments, with the recorded (game-relative) files. */
|
|
10
|
+
readonly command: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export type CrashSymbolUploadResult = {
|
|
13
|
+
readonly status: 'not-applicable';
|
|
14
|
+
readonly reason: string;
|
|
15
|
+
} | {
|
|
16
|
+
readonly status: 'already-uploaded';
|
|
17
|
+
readonly upload: CrashSymbolUploadRecord;
|
|
18
|
+
} | {
|
|
19
|
+
readonly status: 'planned';
|
|
20
|
+
readonly plan: CrashSymbolUploadPlan;
|
|
21
|
+
} | {
|
|
22
|
+
readonly status: 'uploaded';
|
|
23
|
+
readonly plan: CrashSymbolUploadPlan;
|
|
24
|
+
/** Absent when the upload succeeded but its outcome could not be recorded. */
|
|
25
|
+
readonly upload?: CrashSymbolUploadRecord;
|
|
26
|
+
/** Why the successful upload could not be recorded; a retry uploads again. */
|
|
27
|
+
readonly recordError?: string;
|
|
28
|
+
readonly output: string;
|
|
29
|
+
} | {
|
|
30
|
+
readonly status: 'failed';
|
|
31
|
+
readonly plan: CrashSymbolUploadPlan;
|
|
32
|
+
readonly error: string;
|
|
33
|
+
readonly output: string;
|
|
34
|
+
/** Absent when the failure itself could not be recorded. */
|
|
35
|
+
readonly upload?: CrashSymbolUploadRecord;
|
|
36
|
+
};
|
|
37
|
+
export interface UploadRecordedCrashSymbolsInput {
|
|
38
|
+
readonly gameRoot: string;
|
|
39
|
+
readonly gameId: string;
|
|
40
|
+
readonly releaseKey: string;
|
|
41
|
+
/** Print the planned upload; run nothing and record nothing. */
|
|
42
|
+
readonly dryRun?: boolean;
|
|
43
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
44
|
+
readonly signal?: AbortSignal;
|
|
45
|
+
}
|
|
46
|
+
/** Injection boundary for tests; production uses the release state and a child process. */
|
|
47
|
+
export interface CrashSymbolUploadPorts {
|
|
48
|
+
readonly readStatus: typeof readNativeReleaseStatus;
|
|
49
|
+
readonly recordUpload: typeof recordCrashSymbolUpload;
|
|
50
|
+
readonly run: (input: ReleaseProcessInput) => Promise<ReleaseProcessResult>;
|
|
51
|
+
readonly platform: NodeJS.Platform;
|
|
52
|
+
}
|
|
53
|
+
/** Upload (or, with `dryRun`, plan) the Crashlytics dSYMs of one recorded iOS build. */
|
|
54
|
+
export declare function uploadRecordedCrashSymbols(input: UploadRecordedCrashSymbolsInput, ports?: CrashSymbolUploadPorts): Promise<CrashSymbolUploadResult>;
|
|
55
|
+
/** The command that retries a failed upload for one release, for the game directory used. */
|
|
56
|
+
export declare function crashSymbolsRetryCommand(input: {
|
|
57
|
+
readonly gameRoot: string;
|
|
58
|
+
readonly gameId: string;
|
|
59
|
+
readonly releaseKey: string;
|
|
60
|
+
}): string;
|
|
61
|
+
/**
|
|
62
|
+
* The `mpgd deploy run` step: upload the recorded dSYMs of the release's iOS
|
|
63
|
+
* build once. A failure (or an error reading the files or the state) is
|
|
64
|
+
* reported with the retry command and never thrown, so it cannot fail or
|
|
65
|
+
* block the store submission.
|
|
66
|
+
*/
|
|
67
|
+
export declare function uploadCrashSymbolsForDeployRun(input: UploadRecordedCrashSymbolsInput, report?: (message: string) => void, upload?: typeof uploadRecordedCrashSymbols): Promise<CrashSymbolUploadResult | undefined>;
|