@mpgd/cli 0.62.0 → 0.64.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 +14 -0
- package/dist/app-store-iap-asc.js +219 -0
- package/dist/app-store-iap-port.d.ts +125 -0
- package/dist/app-store-iap-port.js +14 -0
- package/dist/app-store-presence-asc.d.ts +23 -0
- package/dist/app-store-presence-asc.js +162 -0
- package/dist/app-store-presence-port.d.ts +101 -0
- package/dist/app-store-presence-port.js +13 -0
- package/dist/asc-session.d.ts +43 -0
- package/dist/asc-session.js +118 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +169 -1
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +411 -339
- package/dist/play-listing-port.d.ts +88 -0
- package/dist/play-listing-port.js +12 -0
- package/dist/play-product-port.d.ts +101 -0
- package/dist/play-product-port.js +7 -0
- package/dist/play-sdk-adapter.js +168 -15
- package/dist/store-presence-app-store.d.ts +139 -0
- package/dist/store-presence-app-store.js +537 -0
- package/dist/store-presence-command.d.ts +50 -0
- package/dist/store-presence-command.js +145 -0
- package/dist/store-presence-play.d.ts +116 -0
- package/dist/store-presence-play.js +495 -0
- package/dist/store-presence.d.ts +106 -0
- package/dist/store-presence.js +173 -0
- 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 +51 -0
- package/dist/store-products-command.js +155 -0
- package/dist/store-products-play.d.ts +112 -0
- package/dist/store-products-play.js +622 -0
- package/dist/store-products.d.ts +38 -0
- package/dist/store-products.js +56 -0
- package/dist/store-sync.d.ts +50 -0
- package/dist/store-sync.js +137 -0
- package/dist/testflight-submission.d.ts +1 -1
- package/package.json +13 -13
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AscIapPort } from './app-store-iap-port.js';
|
|
2
|
+
import { type AscCommandRunner, type AscCredential } from './asc-session.js';
|
|
3
|
+
export type { AscCommandRunner, AscCredential } from './asc-session.js';
|
|
4
|
+
/**
|
|
5
|
+
* The in-app purchase port on the pinned `asc` 5.5.0 CLI (`asc-session.ts`).
|
|
6
|
+
* Each method is one `asc ... --output json` command.
|
|
7
|
+
*/
|
|
8
|
+
/** Copy and verify the pinned binary; `close` removes it. */
|
|
9
|
+
export declare function openAscIapSession(credential: AscCredential, signal?: AbortSignal): Promise<{
|
|
10
|
+
readonly port: AscIapPort;
|
|
11
|
+
close(): void;
|
|
12
|
+
}>;
|
|
13
|
+
/** The port over any runner; the tests drive it with recorded asc output. */
|
|
14
|
+
export declare function createAscIapPort(run: AscCommandRunner, today?: () => string): AscIapPort;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { ascExitNotFound, isEmptyResource, isRecord, list, ascListTimeoutMs as listTimeoutMs, ascLookupTimeoutMs as lookupTimeoutMs, openAscSession, optionalString, relationshipId, requiredString, single, ascUploadTimeoutMs as uploadTimeoutMs, } from './asc-session.js';
|
|
2
|
+
import { ReleaseProcessError } from './deploy-process.js';
|
|
3
|
+
import { sameDecimal } from './store-products.js';
|
|
4
|
+
/**
|
|
5
|
+
* The in-app purchase port on the pinned `asc` 5.5.0 CLI (`asc-session.ts`).
|
|
6
|
+
* Each method is one `asc ... --output json` command.
|
|
7
|
+
*/
|
|
8
|
+
/** Copy and verify the pinned binary; `close` removes it. */
|
|
9
|
+
export async function openAscIapSession(credential, signal) {
|
|
10
|
+
const session = await openAscSession(credential, signal);
|
|
11
|
+
return { port: createAscIapPort(session.run), close: () => session.close() };
|
|
12
|
+
}
|
|
13
|
+
/** The port over any runner; the tests drive it with recorded asc output. */
|
|
14
|
+
export function createAscIapPort(run, today = () => new Date().toISOString().slice(0, 10)) {
|
|
15
|
+
const optional = async (args) => {
|
|
16
|
+
try {
|
|
17
|
+
return await run(args, lookupTimeoutMs);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error instanceof ReleaseProcessError && error.exitCode === ascExitNotFound) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
async readAppBundleId(appId) {
|
|
28
|
+
const app = single(await run(['apps', 'view', '--id', appId], lookupTimeoutMs), 'apps view');
|
|
29
|
+
const bundleId = app.attributes.bundleId;
|
|
30
|
+
if (app.id !== appId || typeof bundleId !== 'string') {
|
|
31
|
+
throw new Error('asc apps view returned no bundle ID for the app.');
|
|
32
|
+
}
|
|
33
|
+
return bundleId;
|
|
34
|
+
},
|
|
35
|
+
async listIaps(appId) {
|
|
36
|
+
return list(await run(['iap', 'list', '--app', appId, '--paginate'], listTimeoutMs), 'iap list')
|
|
37
|
+
.map((resource) => ({
|
|
38
|
+
id: resource.id,
|
|
39
|
+
productId: requiredString(resource.attributes.productId, 'iap list productId'),
|
|
40
|
+
name: requiredString(resource.attributes.name, 'iap list name'),
|
|
41
|
+
inAppPurchaseType: requiredString(resource.attributes.inAppPurchaseType, 'iap list type'),
|
|
42
|
+
...optionalString('state', resource.attributes.state),
|
|
43
|
+
...optionalString('reviewNote', resource.attributes.reviewNote),
|
|
44
|
+
...(resource.attributes.familySharable === true ? { familySharable: true } : {}),
|
|
45
|
+
}));
|
|
46
|
+
},
|
|
47
|
+
async listVersions(iapId) {
|
|
48
|
+
return list(await run(['iap', 'versions', 'list', '--iap-id', iapId, '--paginate'], listTimeoutMs), 'iap versions list').map((resource) => ({
|
|
49
|
+
id: resource.id,
|
|
50
|
+
...(typeof resource.attributes.version === 'number' ? { version: resource.attributes.version } : {}),
|
|
51
|
+
...optionalString('state', resource.attributes.state),
|
|
52
|
+
}));
|
|
53
|
+
},
|
|
54
|
+
async listLocalizations(versionId) {
|
|
55
|
+
return list(await run(['iap', 'versions', 'localizations', 'list', '--version-id', versionId, '--paginate'], listTimeoutMs), 'iap versions localizations list').map((resource) => ({
|
|
56
|
+
id: resource.id,
|
|
57
|
+
locale: requiredString(resource.attributes.locale, 'localization locale'),
|
|
58
|
+
name: requiredString(resource.attributes.name, 'localization name'),
|
|
59
|
+
...optionalString('description', resource.attributes.description),
|
|
60
|
+
}));
|
|
61
|
+
},
|
|
62
|
+
async readPriceSchedule(iapId) {
|
|
63
|
+
const response = await optional(['iap', 'pricing', 'schedules', 'view', '--iap-id', iapId, '--include', 'baseTerritory']);
|
|
64
|
+
// No schedule: asc exits 4 (not found), or App Store Connect answers with no resource.
|
|
65
|
+
if (response === undefined || isEmptyResource(response)) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const schedule = single(response, 'iap pricing schedules view');
|
|
69
|
+
const baseTerritory = relationshipId(schedule.relationships, 'baseTerritory');
|
|
70
|
+
const resolved = await run(['iap', 'pricing', 'schedules', 'manual-prices', '--schedule-id', schedule.id, '--resolved'], listTimeoutMs);
|
|
71
|
+
if (!isRecord(resolved) || !Array.isArray(resolved.prices)) {
|
|
72
|
+
throw new Error('asc iap pricing schedules manual-prices returned no prices.');
|
|
73
|
+
}
|
|
74
|
+
const manualPrices = resolved.prices.flatMap((row) => isRecord(row) && row.manual === true && typeof row.territory === 'string'
|
|
75
|
+
&& typeof row.customerPrice === 'string'
|
|
76
|
+
? [{ territory: row.territory, customerPrice: row.customerPrice }]
|
|
77
|
+
: []);
|
|
78
|
+
if (baseTerritory === undefined && manualPrices.length === 0) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
// `--resolved` lists only prices in effect; the raw list also holds future ones.
|
|
82
|
+
const all = list(await run(['iap', 'pricing', 'schedules', 'manual-prices', '--schedule-id', schedule.id, '--paginate'], listTimeoutMs), 'iap pricing schedules manual-prices');
|
|
83
|
+
const now = today();
|
|
84
|
+
// A future start or a future end (a temporary price expiring) is a
|
|
85
|
+
// transition a new open-ended schedule would remove.
|
|
86
|
+
const upcoming = (value) => typeof value === 'string' && value.slice(0, 10) > now;
|
|
87
|
+
const scheduledChanges = all.filter((price) => upcoming(price.attributes.startDate) || upcoming(price.attributes.endDate)).length;
|
|
88
|
+
return {
|
|
89
|
+
id: schedule.id,
|
|
90
|
+
...(baseTerritory === undefined ? {} : { baseTerritory }),
|
|
91
|
+
manualPrices,
|
|
92
|
+
scheduledChanges,
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
async readAvailability(iapId) {
|
|
96
|
+
let response;
|
|
97
|
+
try {
|
|
98
|
+
response = await run(['iap', 'pricing', 'availability', 'view', '--iap-id', iapId], lookupTimeoutMs);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
// asc 5.5.0 reports an unset availability as an error naming it (availability.go).
|
|
102
|
+
if (error instanceof ReleaseProcessError
|
|
103
|
+
&& (error.exitCode === ascExitNotFound || error.output.includes('has no availability configured'))) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
if (isEmptyResource(response)) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const availability = single(response, 'iap pricing availability view');
|
|
112
|
+
const territories = list(await run(['iap', 'pricing', 'availabilities', 'available-territories', '--id', availability.id, '--paginate'], listTimeoutMs), 'iap pricing availabilities available-territories').map((territory) => territory.id);
|
|
113
|
+
return {
|
|
114
|
+
id: availability.id,
|
|
115
|
+
availableInNewTerritories: availability.attributes.availableInNewTerritories === true,
|
|
116
|
+
territories,
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
async readReviewScreenshot(iapId) {
|
|
120
|
+
const response = await optional(['iap', 'review-screenshots', 'view', '--iap-id', iapId]);
|
|
121
|
+
if (!isRecord(response) || !isRecord(response.data) || typeof response.data.id !== 'string'
|
|
122
|
+
|| response.data.id === '') {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
const attributes = isRecord(response.data.attributes) ? response.data.attributes : {};
|
|
126
|
+
return {
|
|
127
|
+
id: response.data.id,
|
|
128
|
+
...optionalString('fileName', attributes.fileName),
|
|
129
|
+
...(typeof attributes.fileSize === 'number' ? { fileSize: attributes.fileSize } : {}),
|
|
130
|
+
...optionalString('sourceFileChecksum', attributes.sourceFileChecksum),
|
|
131
|
+
...optionalString('deliveryState', isRecord(attributes.assetDeliveryState) ? attributes.assetDeliveryState.state : undefined),
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
async listTerritories() {
|
|
135
|
+
const territories = list(await run(['pricing', 'territories', 'list', '--paginate'], listTimeoutMs), 'pricing territories list').map((resource) => resource.id);
|
|
136
|
+
if (territories.length === 0) {
|
|
137
|
+
throw new Error('asc pricing territories list returned no territories.');
|
|
138
|
+
}
|
|
139
|
+
return territories;
|
|
140
|
+
},
|
|
141
|
+
async createIap(input) {
|
|
142
|
+
const created = single(await run([
|
|
143
|
+
'iap', 'create',
|
|
144
|
+
'--app', input.appId,
|
|
145
|
+
'--type', input.type,
|
|
146
|
+
'--ref-name', input.referenceName,
|
|
147
|
+
'--product-id', input.productId,
|
|
148
|
+
...(input.familySharable ? ['--family-sharable'] : []),
|
|
149
|
+
], lookupTimeoutMs), 'iap create');
|
|
150
|
+
return created.id;
|
|
151
|
+
},
|
|
152
|
+
async updateIap(input) {
|
|
153
|
+
await run([
|
|
154
|
+
'iap', 'update',
|
|
155
|
+
'--id', input.iapId,
|
|
156
|
+
...(input.referenceName === undefined ? [] : ['--ref-name', input.referenceName]),
|
|
157
|
+
...(input.familySharable === true ? ['--family-sharable'] : []),
|
|
158
|
+
], lookupTimeoutMs);
|
|
159
|
+
},
|
|
160
|
+
async createVersion(iapId) {
|
|
161
|
+
return single(await run(['iap', 'versions', 'create', '--iap-id', iapId], lookupTimeoutMs), 'iap versions create').id;
|
|
162
|
+
},
|
|
163
|
+
async createLocalization(input) {
|
|
164
|
+
await run([
|
|
165
|
+
'iap', 'versions', 'localizations', 'create',
|
|
166
|
+
'--version-id', input.versionId,
|
|
167
|
+
'--locale', input.locale,
|
|
168
|
+
'--name', input.name,
|
|
169
|
+
'--description', input.description,
|
|
170
|
+
], lookupTimeoutMs);
|
|
171
|
+
},
|
|
172
|
+
async updateLocalization(input) {
|
|
173
|
+
await run([
|
|
174
|
+
'iap', 'versions', 'localizations', 'update',
|
|
175
|
+
'--localization-id', input.localizationId,
|
|
176
|
+
'--name', input.name,
|
|
177
|
+
'--description', input.description,
|
|
178
|
+
], lookupTimeoutMs);
|
|
179
|
+
},
|
|
180
|
+
async findPricePoint(input) {
|
|
181
|
+
const points = list(await run([
|
|
182
|
+
'iap', 'pricing', 'price-points', 'list',
|
|
183
|
+
'--iap-id', input.iapId,
|
|
184
|
+
'--territory', input.territory,
|
|
185
|
+
'--price', input.customerPrice,
|
|
186
|
+
'--paginate',
|
|
187
|
+
], listTimeoutMs), 'iap pricing price-points list').filter((point) => typeof point.attributes.customerPrice === 'string'
|
|
188
|
+
&& sameDecimal(point.attributes.customerPrice, input.customerPrice));
|
|
189
|
+
const [point] = points;
|
|
190
|
+
if (points.length !== 1 || point === undefined) {
|
|
191
|
+
throw new Error(`App Store Connect has ${points.length} price points of ${input.customerPrice} in ${input.territory}; `
|
|
192
|
+
+ 'use a price that is an App Store price point there.');
|
|
193
|
+
}
|
|
194
|
+
return point.id;
|
|
195
|
+
},
|
|
196
|
+
async createPriceSchedule(input) {
|
|
197
|
+
await run([
|
|
198
|
+
'iap', 'pricing', 'schedules', 'create',
|
|
199
|
+
'--iap-id', input.iapId,
|
|
200
|
+
'--base-territory', input.baseTerritory,
|
|
201
|
+
'--prices', input.pricePointIds.join(','),
|
|
202
|
+
], lookupTimeoutMs);
|
|
203
|
+
},
|
|
204
|
+
async setAvailability(input) {
|
|
205
|
+
await run([
|
|
206
|
+
'iap', 'pricing', 'availability', 'set',
|
|
207
|
+
'--iap-id', input.iapId,
|
|
208
|
+
'--territories', input.territories.join(','),
|
|
209
|
+
...(input.availableInNewTerritories ? ['--available-in-new-territories'] : []),
|
|
210
|
+
], lookupTimeoutMs);
|
|
211
|
+
},
|
|
212
|
+
async deleteReviewScreenshot(screenshotId) {
|
|
213
|
+
await run(['iap', 'review-screenshots', 'delete', '--screenshot-id', screenshotId, '--confirm'], lookupTimeoutMs);
|
|
214
|
+
},
|
|
215
|
+
async uploadReviewScreenshot(input) {
|
|
216
|
+
await run(['iap', 'review-screenshots', 'create', '--iap-id', input.iapId, '--file', input.file], uploadTimeoutMs);
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -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 {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AscStorePresencePort } from './app-store-presence-port.js';
|
|
2
|
+
import { type AscCommandRunner, type AscCredential } from './asc-session.js';
|
|
3
|
+
/**
|
|
4
|
+
* The store page port on the pinned `asc` 5.5.0 CLI. Commands (5.5.0 source,
|
|
5
|
+
* `internal/cli/...`):
|
|
6
|
+
*
|
|
7
|
+
* - `apps view`, `apps info list` (`apps/app_infos.go`)
|
|
8
|
+
* - `localizations list --type app-info` and `--version` (`localizations/localizations.go`)
|
|
9
|
+
* - `app-setup info set` (`apps/app_setup.go`): creates or updates one app
|
|
10
|
+
* information localization
|
|
11
|
+
* - `versions list` (`versions/versions.go`)
|
|
12
|
+
* - `localizations create` and `localizations update --id` (`localizations/create.go`, `update.go`)
|
|
13
|
+
* - `screenshots list|upload|delete` (`assets/assets_screenshots*.go`): an
|
|
14
|
+
* upload of one file appends it to the set and waits for the asset
|
|
15
|
+
* delivery to reach `COMPLETE`, failing on `FAILED`
|
|
16
|
+
*/
|
|
17
|
+
/** Copy and verify the pinned binary; `close` removes it. */
|
|
18
|
+
export declare function openAscStorePresenceSession(credential: AscCredential, signal?: AbortSignal): Promise<{
|
|
19
|
+
readonly port: AscStorePresencePort;
|
|
20
|
+
close(): void;
|
|
21
|
+
}>;
|
|
22
|
+
/** The port over any runner; the tests drive it with recorded asc output. */
|
|
23
|
+
export declare function createAscStorePresencePort(run: AscCommandRunner): AscStorePresencePort;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { ascListTimeoutMs, ascLookupTimeoutMs, ascUploadTimeoutMs, isRecord, list, openAscSession, optionalString, requiredString, single, } from './asc-session.js';
|
|
2
|
+
/**
|
|
3
|
+
* The store page port on the pinned `asc` 5.5.0 CLI. Commands (5.5.0 source,
|
|
4
|
+
* `internal/cli/...`):
|
|
5
|
+
*
|
|
6
|
+
* - `apps view`, `apps info list` (`apps/app_infos.go`)
|
|
7
|
+
* - `localizations list --type app-info` and `--version` (`localizations/localizations.go`)
|
|
8
|
+
* - `app-setup info set` (`apps/app_setup.go`): creates or updates one app
|
|
9
|
+
* information localization
|
|
10
|
+
* - `versions list` (`versions/versions.go`)
|
|
11
|
+
* - `localizations create` and `localizations update --id` (`localizations/create.go`, `update.go`)
|
|
12
|
+
* - `screenshots list|upload|delete` (`assets/assets_screenshots*.go`): an
|
|
13
|
+
* upload of one file appends it to the set and waits for the asset
|
|
14
|
+
* delivery to reach `COMPLETE`, failing on `FAILED`
|
|
15
|
+
*/
|
|
16
|
+
/** Copy and verify the pinned binary; `close` removes it. */
|
|
17
|
+
export async function openAscStorePresenceSession(credential, signal) {
|
|
18
|
+
const session = await openAscSession(credential, signal);
|
|
19
|
+
return { port: createAscStorePresencePort(session.run), close: () => session.close() };
|
|
20
|
+
}
|
|
21
|
+
/** The port over any runner; the tests drive it with recorded asc output. */
|
|
22
|
+
export function createAscStorePresencePort(run) {
|
|
23
|
+
return {
|
|
24
|
+
async readAppBundleId(appId) {
|
|
25
|
+
const app = single(await run(['apps', 'view', '--id', appId], ascLookupTimeoutMs), 'apps view');
|
|
26
|
+
const bundleId = app.attributes.bundleId;
|
|
27
|
+
if (app.id !== appId || typeof bundleId !== 'string') {
|
|
28
|
+
throw new Error('asc apps view returned no bundle ID for the app.');
|
|
29
|
+
}
|
|
30
|
+
return bundleId;
|
|
31
|
+
},
|
|
32
|
+
async listAppInfos(appId) {
|
|
33
|
+
return list(await run(['apps', 'info', 'list', '--app', appId], ascLookupTimeoutMs), 'apps info list')
|
|
34
|
+
.map((resource) => ({
|
|
35
|
+
id: resource.id,
|
|
36
|
+
...optionalString('state', resource.attributes.state ?? resource.attributes.appStoreState),
|
|
37
|
+
}));
|
|
38
|
+
},
|
|
39
|
+
async listAppInfoLocalizations(appId, appInfoId) {
|
|
40
|
+
return list(await run([
|
|
41
|
+
'localizations', 'list',
|
|
42
|
+
'--type', 'app-info',
|
|
43
|
+
'--app', appId,
|
|
44
|
+
'--app-info', appInfoId,
|
|
45
|
+
'--paginate',
|
|
46
|
+
], ascListTimeoutMs), 'localizations list --type app-info').map((resource) => ({
|
|
47
|
+
id: resource.id,
|
|
48
|
+
locale: requiredString(resource.attributes.locale, 'app info localization locale'),
|
|
49
|
+
...optionalString('name', resource.attributes.name),
|
|
50
|
+
...optionalString('subtitle', resource.attributes.subtitle),
|
|
51
|
+
...optionalString('privacyPolicyUrl', resource.attributes.privacyPolicyUrl),
|
|
52
|
+
}));
|
|
53
|
+
},
|
|
54
|
+
async listVersions(appId) {
|
|
55
|
+
return list(await run(['versions', 'list', '--app', appId, '--platform', 'IOS', '--paginate'], ascListTimeoutMs), 'versions list').map((resource) => ({
|
|
56
|
+
id: resource.id,
|
|
57
|
+
...optionalString('versionString', resource.attributes.versionString),
|
|
58
|
+
...optionalString('state', resource.attributes.appVersionState ?? resource.attributes.appStoreState),
|
|
59
|
+
...optionalString('appStoreState', resource.attributes.appStoreState),
|
|
60
|
+
...optionalString('createdDate', resource.attributes.createdDate),
|
|
61
|
+
}));
|
|
62
|
+
},
|
|
63
|
+
async listVersionLocalizations(versionId) {
|
|
64
|
+
return list(await run(['localizations', 'list', '--version', versionId, '--paginate'], ascListTimeoutMs), 'localizations list').map((resource) => ({
|
|
65
|
+
id: resource.id,
|
|
66
|
+
locale: requiredString(resource.attributes.locale, 'version localization locale'),
|
|
67
|
+
...versionFields(resource),
|
|
68
|
+
}));
|
|
69
|
+
},
|
|
70
|
+
async listScreenshotSets(versionLocalizationId) {
|
|
71
|
+
const result = await run(['screenshots', 'list', '--version-localization', versionLocalizationId], ascListTimeoutMs);
|
|
72
|
+
if (!isRecord(result) || !Array.isArray(result.sets)) {
|
|
73
|
+
throw new Error('asc screenshots list returned no screenshot sets.');
|
|
74
|
+
}
|
|
75
|
+
return result.sets.map((entry) => {
|
|
76
|
+
const set = isRecord(entry) ? single({ data: entry.set }, 'screenshots list set') : undefined;
|
|
77
|
+
const screenshots = isRecord(entry) ? list({ data: entry.screenshots }, 'screenshots list') : undefined;
|
|
78
|
+
if (set === undefined || screenshots === undefined) {
|
|
79
|
+
throw new Error('asc screenshots list returned a malformed set.');
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
id: set.id,
|
|
83
|
+
displayType: requiredString(set.attributes.screenshotDisplayType, 'screenshot display type'),
|
|
84
|
+
screenshots: screenshots.map(toScreenshot),
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
async upsertAppInfoLocalization(input) {
|
|
89
|
+
await run([
|
|
90
|
+
'app-setup', 'info', 'set',
|
|
91
|
+
'--app', input.appId,
|
|
92
|
+
'--app-info', input.appInfoId,
|
|
93
|
+
'--locale', input.locale,
|
|
94
|
+
'--name', input.name,
|
|
95
|
+
...(input.subtitle === undefined ? [] : ['--subtitle', input.subtitle]),
|
|
96
|
+
...(input.privacyPolicyUrl === undefined ? [] : ['--privacy-policy-url', input.privacyPolicyUrl]),
|
|
97
|
+
], ascLookupTimeoutMs);
|
|
98
|
+
},
|
|
99
|
+
async createVersionLocalization(input) {
|
|
100
|
+
return single(await run([
|
|
101
|
+
'localizations', 'create',
|
|
102
|
+
'--version', input.versionId,
|
|
103
|
+
'--locale', input.locale,
|
|
104
|
+
...fieldFlags(input.fields),
|
|
105
|
+
], ascLookupTimeoutMs), 'localizations create').id;
|
|
106
|
+
},
|
|
107
|
+
async updateVersionLocalization(input) {
|
|
108
|
+
const flags = fieldFlags(input.fields);
|
|
109
|
+
if (flags.length === 0) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
await run(['localizations', 'update', '--id', input.localizationId, ...flags], ascLookupTimeoutMs);
|
|
113
|
+
},
|
|
114
|
+
async uploadScreenshot(input) {
|
|
115
|
+
const result = await run([
|
|
116
|
+
'screenshots', 'upload',
|
|
117
|
+
'--version-localization', input.versionLocalizationId,
|
|
118
|
+
'--path', input.file,
|
|
119
|
+
'--device-type', input.displayType,
|
|
120
|
+
], ascUploadTimeoutMs);
|
|
121
|
+
const results = isRecord(result) && Array.isArray(result.results) ? result.results : [];
|
|
122
|
+
const [item] = results;
|
|
123
|
+
if (results.length !== 1 || !isRecord(item) || typeof item.assetId !== 'string' || item.assetId === '') {
|
|
124
|
+
throw new Error('asc screenshots upload returned no uploaded screenshot.');
|
|
125
|
+
}
|
|
126
|
+
const state = typeof item.state === 'string' ? item.state.toUpperCase() : '';
|
|
127
|
+
if (state !== 'COMPLETE') {
|
|
128
|
+
throw new Error(`App Store Connect reports screenshot ${item.assetId} as ${state || 'not delivered'}.`);
|
|
129
|
+
}
|
|
130
|
+
return { id: item.assetId, deliveryState: state };
|
|
131
|
+
},
|
|
132
|
+
async deleteScreenshot(screenshotId) {
|
|
133
|
+
await run(['screenshots', 'delete', '--id', screenshotId, '--confirm'], ascLookupTimeoutMs);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const fieldNames = [
|
|
138
|
+
['description', '--description'],
|
|
139
|
+
['keywords', '--keywords'],
|
|
140
|
+
['promotionalText', '--promotional-text'],
|
|
141
|
+
['whatsNew', '--whats-new'],
|
|
142
|
+
['supportUrl', '--support-url'],
|
|
143
|
+
['marketingUrl', '--marketing-url'],
|
|
144
|
+
];
|
|
145
|
+
function fieldFlags(fields) {
|
|
146
|
+
return fieldNames.flatMap(([field, flag]) => {
|
|
147
|
+
const value = fields[field];
|
|
148
|
+
return value === undefined ? [] : [flag, value];
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function versionFields(resource) {
|
|
152
|
+
return Object.assign({}, ...fieldNames.map(([field]) => optionalString(field, resource.attributes[field])));
|
|
153
|
+
}
|
|
154
|
+
function toScreenshot(resource) {
|
|
155
|
+
const delivery = resource.attributes.assetDeliveryState;
|
|
156
|
+
return {
|
|
157
|
+
id: resource.id,
|
|
158
|
+
...optionalString('fileName', resource.attributes.fileName),
|
|
159
|
+
...optionalString('sourceFileChecksum', resource.attributes.sourceFileChecksum),
|
|
160
|
+
...optionalString('deliveryState', isRecord(delivery) ? delivery.state : undefined),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Store Connect store page port: app information localizations (name,
|
|
3
|
+
* subtitle, privacy policy URL), App Store version localizations (description,
|
|
4
|
+
* keywords, promotional text, what's new, URLs) and their screenshot sets.
|
|
5
|
+
* `app-store-presence-asc.ts` implements it with the pinned `asc` CLI; the
|
|
6
|
+
* planner and tests see only these shapes.
|
|
7
|
+
*
|
|
8
|
+
* Resources (App Store Connect API):
|
|
9
|
+
* - App infos and their localizations: https://developer.apple.com/documentation/appstoreconnectapi/app-infos
|
|
10
|
+
* - App Store versions and their localizations: https://developer.apple.com/documentation/appstoreconnectapi/app-store-versions
|
|
11
|
+
* - Screenshot sets and screenshots: https://developer.apple.com/documentation/appstoreconnectapi/app-screenshot-sets
|
|
12
|
+
*/
|
|
13
|
+
export interface AscAppInfo {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
/** `AppInfoState` (`state`, or the older `appStoreState`), for example `PREPARE_FOR_SUBMISSION`. */
|
|
16
|
+
readonly state?: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
export interface AscAppInfoLocalization {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly locale: string;
|
|
21
|
+
readonly name?: string | undefined;
|
|
22
|
+
readonly subtitle?: string | undefined;
|
|
23
|
+
readonly privacyPolicyUrl?: string | undefined;
|
|
24
|
+
}
|
|
25
|
+
export interface AscAppStoreVersion {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly versionString?: string | undefined;
|
|
28
|
+
/** `appVersionState`, or `appStoreState` when App Store Connect sends only that. */
|
|
29
|
+
readonly state?: string | undefined;
|
|
30
|
+
/** The legacy `appStoreState`; App Store Connect fills the two inconsistently. */
|
|
31
|
+
readonly appStoreState?: string | undefined;
|
|
32
|
+
readonly createdDate?: string | undefined;
|
|
33
|
+
}
|
|
34
|
+
/** The version localization fields the sync writes. */
|
|
35
|
+
export interface AscVersionLocalizationFields {
|
|
36
|
+
readonly description?: string | undefined;
|
|
37
|
+
readonly keywords?: string | undefined;
|
|
38
|
+
readonly promotionalText?: string | undefined;
|
|
39
|
+
readonly whatsNew?: string | undefined;
|
|
40
|
+
readonly supportUrl?: string | undefined;
|
|
41
|
+
readonly marketingUrl?: string | undefined;
|
|
42
|
+
}
|
|
43
|
+
export interface AscVersionLocalization extends AscVersionLocalizationFields {
|
|
44
|
+
readonly id: string;
|
|
45
|
+
readonly locale: string;
|
|
46
|
+
}
|
|
47
|
+
export interface AscScreenshot {
|
|
48
|
+
readonly id: string;
|
|
49
|
+
readonly fileName?: string | undefined;
|
|
50
|
+
/** MD5 of the uploaded file, as App Store Connect records it. */
|
|
51
|
+
readonly sourceFileChecksum?: string | undefined;
|
|
52
|
+
/** `assetDeliveryState.state`: `AWAITING_UPLOAD`, `UPLOAD_COMPLETE`, `COMPLETE` or `FAILED`. */
|
|
53
|
+
readonly deliveryState?: string | undefined;
|
|
54
|
+
}
|
|
55
|
+
export interface AscScreenshotSet {
|
|
56
|
+
readonly id: string;
|
|
57
|
+
/** `screenshotDisplayType`, for example `APP_IPHONE_67`. */
|
|
58
|
+
readonly displayType: string;
|
|
59
|
+
/** In the set's order. */
|
|
60
|
+
readonly screenshots: readonly AscScreenshot[];
|
|
61
|
+
}
|
|
62
|
+
export interface AscStorePresencePort {
|
|
63
|
+
/** The app record's bundle ID (`apps view`). */
|
|
64
|
+
readAppBundleId(appId: string): Promise<string>;
|
|
65
|
+
listAppInfos(appId: string): Promise<AscAppInfo[]>;
|
|
66
|
+
listAppInfoLocalizations(appId: string, appInfoId: string): Promise<AscAppInfoLocalization[]>;
|
|
67
|
+
/** iOS App Store versions of the app. */
|
|
68
|
+
listVersions(appId: string): Promise<AscAppStoreVersion[]>;
|
|
69
|
+
listVersionLocalizations(versionId: string): Promise<AscVersionLocalization[]>;
|
|
70
|
+
listScreenshotSets(versionLocalizationId: string): Promise<AscScreenshotSet[]>;
|
|
71
|
+
/** Creates the locale's app information localization or updates it. */
|
|
72
|
+
upsertAppInfoLocalization(input: {
|
|
73
|
+
readonly appId: string;
|
|
74
|
+
readonly appInfoId: string;
|
|
75
|
+
readonly locale: string;
|
|
76
|
+
readonly name: string;
|
|
77
|
+
readonly subtitle?: string;
|
|
78
|
+
readonly privacyPolicyUrl?: string;
|
|
79
|
+
}): Promise<void>;
|
|
80
|
+
/** Returns the new localization's ID. */
|
|
81
|
+
createVersionLocalization(input: {
|
|
82
|
+
readonly versionId: string;
|
|
83
|
+
readonly locale: string;
|
|
84
|
+
readonly fields: AscVersionLocalizationFields;
|
|
85
|
+
}): Promise<string>;
|
|
86
|
+
updateVersionLocalization(input: {
|
|
87
|
+
readonly localizationId: string;
|
|
88
|
+
readonly fields: AscVersionLocalizationFields;
|
|
89
|
+
}): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Appends one screenshot to the set of `displayType` (creating the set),
|
|
92
|
+
* waiting until App Store Connect reports the asset delivered. Throws when
|
|
93
|
+
* the delivery fails.
|
|
94
|
+
*/
|
|
95
|
+
uploadScreenshot(input: {
|
|
96
|
+
readonly versionLocalizationId: string;
|
|
97
|
+
readonly displayType: string;
|
|
98
|
+
readonly file: string;
|
|
99
|
+
}): Promise<AscScreenshot>;
|
|
100
|
+
deleteScreenshot(screenshotId: string): Promise<void>;
|
|
101
|
+
}
|