eas-cli 18.5.0 → 18.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +90 -89
  2. package/build/commandUtils/pagination.d.ts +2 -1
  3. package/build/commandUtils/pagination.js +3 -2
  4. package/build/commands/build/dev.d.ts +1 -0
  5. package/build/commands/build/dev.js +10 -2
  6. package/build/commands/deploy/index.js +18 -2
  7. package/build/commands/metadata/pull.d.ts +1 -1
  8. package/build/commands/metadata/pull.js +2 -4
  9. package/build/commands/metadata/push.d.ts +1 -1
  10. package/build/commands/metadata/push.js +2 -4
  11. package/build/commands/observe/events.d.ts +27 -0
  12. package/build/commands/observe/events.js +140 -0
  13. package/build/commands/observe/metrics.d.ts +21 -0
  14. package/build/commands/observe/metrics.js +111 -0
  15. package/build/commands/observe/versions.d.ts +19 -0
  16. package/build/commands/observe/versions.js +69 -0
  17. package/build/credentials/ios/IosCredentialsProvider.js +8 -4
  18. package/build/credentials/ios/utils/provisioningProfile.d.ts +1 -0
  19. package/build/credentials/ios/utils/provisioningProfile.js +15 -1
  20. package/build/graphql/generated.d.ts +634 -61
  21. package/build/graphql/generated.js +11 -9
  22. package/build/graphql/queries/ObserveQuery.d.ts +35 -0
  23. package/build/graphql/queries/ObserveQuery.js +109 -0
  24. package/build/graphql/types/Observe.d.ts +3 -0
  25. package/build/graphql/types/Observe.js +84 -0
  26. package/build/metadata/apple/config/reader.d.ts +9 -1
  27. package/build/metadata/apple/config/reader.js +25 -0
  28. package/build/metadata/apple/config/writer.d.ts +7 -1
  29. package/build/metadata/apple/config/writer.js +44 -0
  30. package/build/metadata/apple/data.d.ts +2 -1
  31. package/build/metadata/apple/tasks/app-clip.d.ts +37 -0
  32. package/build/metadata/apple/tasks/app-clip.js +404 -0
  33. package/build/metadata/apple/tasks/index.js +2 -0
  34. package/build/metadata/apple/tasks/previews.js +16 -12
  35. package/build/metadata/apple/tasks/screenshots.js +15 -4
  36. package/build/metadata/apple/types.d.ts +28 -1
  37. package/build/observe/fetchEvents.d.ts +27 -0
  38. package/build/observe/fetchEvents.js +83 -0
  39. package/build/observe/fetchMetrics.d.ts +11 -0
  40. package/build/observe/fetchMetrics.js +78 -0
  41. package/build/observe/fetchVersions.d.ts +7 -0
  42. package/build/observe/fetchVersions.js +31 -0
  43. package/build/observe/formatEvents.d.ts +31 -0
  44. package/build/observe/formatEvents.js +99 -0
  45. package/build/observe/formatMetrics.d.ts +38 -0
  46. package/build/observe/formatMetrics.js +206 -0
  47. package/build/observe/formatVersions.d.ts +32 -0
  48. package/build/observe/formatVersions.js +92 -0
  49. package/build/observe/metricNames.d.ts +4 -0
  50. package/build/observe/metricNames.js +33 -0
  51. package/build/observe/startAndEndTime.d.ts +18 -0
  52. package/build/observe/startAndEndTime.js +36 -0
  53. package/build/user/SessionManager.js +11 -0
  54. package/build/worker/upload.js +15 -5
  55. package/oclif.manifest.json +2102 -1620
  56. package/package.json +9 -6
  57. package/schema/metadata-0.json +177 -0
@@ -0,0 +1,404 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AppClipTask = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const apple_utils_1 = require("@expo/apple-utils");
6
+ const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
+ const fs_1 = tslib_1.__importDefault(require("fs"));
8
+ const path_1 = tslib_1.__importDefault(require("path"));
9
+ const fetch_1 = tslib_1.__importDefault(require("../../../fetch"));
10
+ const log_1 = tslib_1.__importDefault(require("../../../log"));
11
+ const log_2 = require("../../utils/log");
12
+ const task_1 = require("../task");
13
+ /**
14
+ * Task for managing App Clip metadata (default experience, localized
15
+ * subtitles + header images, App Store review invocation URLs).
16
+ *
17
+ * No-op when the app does not have an App Clip target.
18
+ */
19
+ class AppClipTask extends task_1.AppleTask {
20
+ name = () => 'app clip';
21
+ async prepareAsync({ context }) {
22
+ context.appClip = null;
23
+ context.appClipDefaultExperience = null;
24
+ context.appClipTemplateExperience = null;
25
+ context.appClipLocalizations = new Map();
26
+ context.appClipHeaderImages = new Map();
27
+ context.appClipReviewDetail = null;
28
+ const appClips = await context.app.getAppClipsAsync();
29
+ if (appClips.length === 0) {
30
+ return;
31
+ }
32
+ // Apps may have multiple App Clip bundles registered. Pick the first
33
+ // clip that has any default experiences attached.
34
+ let chosenClip = null;
35
+ let allExperiences = [];
36
+ for (const clip of appClips) {
37
+ const experiences = await clip.getAppClipDefaultExperiencesAsync();
38
+ if (experiences.length > 0) {
39
+ chosenClip = clip;
40
+ allExperiences = experiences;
41
+ break;
42
+ }
43
+ }
44
+ chosenClip ??= appClips[0];
45
+ context.appClip = chosenClip;
46
+ if (allExperiences.length === 0) {
47
+ return;
48
+ }
49
+ // App Clip default experiences are versioned per app store version. Find
50
+ // the experience linked to the current editable version. Fall back to the
51
+ // first experience as a template (used as the source of truth on pull
52
+ // when the current version has no experience yet, and as a template when
53
+ // creating a new experience on push).
54
+ const currentVersionId = context.version?.id;
55
+ const currentVersionExperience = currentVersionId
56
+ ? (allExperiences.find(exp => exp.attributes.releaseWithAppStoreVersion?.id === currentVersionId) ?? null)
57
+ : null;
58
+ context.appClipDefaultExperience = currentVersionExperience;
59
+ // The template is only used when we need to CREATE a new default
60
+ // experience for the current version on push — pick any other existing
61
+ // experience (most recent first by sort order). When the current version
62
+ // already has an experience, no template is needed.
63
+ context.appClipTemplateExperience = currentVersionExperience
64
+ ? null
65
+ : (allExperiences[0] ?? null);
66
+ // Use the current version's experience for downstream data when present;
67
+ // otherwise fall back to the template so `metadata:pull` still produces a
68
+ // populated `store.config.json`.
69
+ const sourceExperience = currentVersionExperience ?? context.appClipTemplateExperience;
70
+ if (!sourceExperience) {
71
+ return;
72
+ }
73
+ const [localizations, reviewDetail] = await Promise.all([
74
+ sourceExperience.getAppClipDefaultExperienceLocalizationsAsync(),
75
+ sourceExperience.getAppClipAppStoreReviewDetailAsync(),
76
+ ]);
77
+ for (const localization of localizations) {
78
+ context.appClipLocalizations.set(localization.attributes.locale, localization);
79
+ }
80
+ context.appClipReviewDetail = reviewDetail;
81
+ // Header images may be missing from the included payload depending on
82
+ // ASC's whims; fetch any that weren't pre-populated.
83
+ await Promise.all(localizations.map(async (localization) => {
84
+ const included = localization.attributes.appClipHeaderImage;
85
+ if (included) {
86
+ context.appClipHeaderImages.set(localization.attributes.locale, included);
87
+ return;
88
+ }
89
+ const headerImage = await localization.getAppClipHeaderImageAsync();
90
+ if (headerImage) {
91
+ context.appClipHeaderImages.set(localization.attributes.locale, headerImage);
92
+ }
93
+ }));
94
+ }
95
+ async downloadAsync({ config, context }) {
96
+ // Pull data from the current version's experience when present, otherwise
97
+ // fall back to the template experience. The localizations and review
98
+ // detail in `context` were populated from whichever was used.
99
+ const experience = context.appClipDefaultExperience ?? context.appClipTemplateExperience;
100
+ if (!experience) {
101
+ return;
102
+ }
103
+ config.setAppClipDefaultExperience({
104
+ action: experience.attributes.action ?? undefined,
105
+ // ASC does not expose `releaseWithAppStoreVersion` directly on the
106
+ // attributes — it's a relationship. We treat presence of an included
107
+ // version as `true`. When unset we leave the field undefined so the
108
+ // serializer omits it.
109
+ releaseWithAppStoreVersion: experience.attributes.releaseWithAppStoreVersion != null ? true : undefined,
110
+ });
111
+ const reviewDetail = context.appClipReviewDetail;
112
+ if (reviewDetail?.attributes.invocationUrls?.length) {
113
+ config.setAppClipReviewDetail({
114
+ invocationUrls: reviewDetail.attributes.invocationUrls,
115
+ });
116
+ }
117
+ else {
118
+ config.setAppClipReviewDetail(null);
119
+ }
120
+ for (const [locale, localization] of context.appClipLocalizations) {
121
+ const headerImage = context.appClipHeaderImages.get(locale);
122
+ let headerImagePath;
123
+ if (headerImage) {
124
+ const downloaded = await downloadAppClipHeaderImageAsync(context.projectDir, locale, headerImage);
125
+ if (downloaded) {
126
+ headerImagePath = downloaded;
127
+ }
128
+ else {
129
+ // Image exists in ASC but isn't downloadable yet (still processing,
130
+ // or Apple's CDN hasn't caught up). Preserve the expected local path
131
+ // so subsequent pushes don't try to delete the in-progress upload.
132
+ // Filename match in syncAppClipHeaderImageAsync will skip re-upload.
133
+ const fileName = headerImage.attributes.fileName || 'header.png';
134
+ headerImagePath = path_1.default.join('store', 'apple', 'app-clip', locale, fileName);
135
+ }
136
+ }
137
+ config.setAppClipLocalizedInfo(locale, {
138
+ subtitle: localization.attributes.subtitle ?? undefined,
139
+ headerImage: headerImagePath,
140
+ });
141
+ }
142
+ }
143
+ async uploadAsync({ config, context }) {
144
+ const desired = config.getAppClipDefaultExperience();
145
+ if (!desired) {
146
+ log_1.default.log((0, chalk_1.default) `{dim - Skipped app clip, not configured}`);
147
+ return;
148
+ }
149
+ if (!context.appClip) {
150
+ log_1.default.warn((0, chalk_1.default) `{yellow Skipping app clip - no App Clip is registered for this app in App Store Connect}`);
151
+ return;
152
+ }
153
+ if (!context.version) {
154
+ log_1.default.warn((0, chalk_1.default) `{yellow Skipping app clip - no editable app store version available}`);
155
+ return;
156
+ }
157
+ // App Clip default experiences are versioned per app store version. If
158
+ // the current editable version doesn't have one yet, create a new one,
159
+ // optionally cloning the most recent prior experience as a template.
160
+ let experience = context.appClipDefaultExperience;
161
+ // We always link the default experience to the current editable version,
162
+ // regardless of whether the user opted into `releaseWithAppStoreVersion`
163
+ // — every default experience must belong to a version in ASC.
164
+ const releaseWithAppStoreVersionId = context.version.id;
165
+ if (!experience) {
166
+ const appClipId = context.appClip.id;
167
+ const templateId = context.appClipTemplateExperience?.id;
168
+ experience = await (0, log_2.logAsync)(() => apple_utils_1.AppClipDefaultExperience.createAsync(context.app.context, {
169
+ appClipId,
170
+ releaseWithAppStoreVersionId,
171
+ appClipDefaultExperienceTemplateId: templateId,
172
+ attributes: { action: desired.action ?? null },
173
+ }), {
174
+ pending: templateId
175
+ ? `Creating App Clip default experience for ${chalk_1.default.bold(context.version.attributes.versionString)} (cloned from previous version)...`
176
+ : `Creating App Clip default experience for ${chalk_1.default.bold(context.version.attributes.versionString)}...`,
177
+ success: `Created App Clip default experience for ${chalk_1.default.bold(context.version.attributes.versionString)}`,
178
+ failure: `Failed creating App Clip default experience for ${chalk_1.default.bold(context.version.attributes.versionString)}`,
179
+ });
180
+ context.appClipDefaultExperience = experience;
181
+ // Apple cloned the previous experience's localizations + review detail
182
+ // into the new experience. Re-fetch them so subsequent diff/sync logic
183
+ // operates on the new resource ids.
184
+ const [localizations, reviewDetail] = await Promise.all([
185
+ experience.getAppClipDefaultExperienceLocalizationsAsync(),
186
+ experience.getAppClipAppStoreReviewDetailAsync(),
187
+ ]);
188
+ context.appClipLocalizations = new Map();
189
+ context.appClipHeaderImages = new Map();
190
+ for (const localization of localizations) {
191
+ context.appClipLocalizations.set(localization.attributes.locale, localization);
192
+ const included = localization.attributes.appClipHeaderImage;
193
+ if (included) {
194
+ context.appClipHeaderImages.set(localization.attributes.locale, included);
195
+ }
196
+ else {
197
+ const headerImage = await localization.getAppClipHeaderImageAsync();
198
+ if (headerImage) {
199
+ context.appClipHeaderImages.set(localization.attributes.locale, headerImage);
200
+ }
201
+ }
202
+ }
203
+ context.appClipReviewDetail = reviewDetail;
204
+ }
205
+ else {
206
+ const currentAction = experience.attributes.action ?? null;
207
+ const desiredAction = desired.action ?? null;
208
+ // Apple rejects PATCHes on `action` once the linked app store version is
209
+ // locked (in review / released). Skip the call when nothing actually
210
+ // changed to avoid spurious failures on round-trip pushes.
211
+ if (currentAction !== desiredAction) {
212
+ const existing = experience;
213
+ experience = await (0, log_2.logAsync)(() => existing.updateAsync({
214
+ action: desiredAction,
215
+ releaseWithAppStoreVersionId: desired.releaseWithAppStoreVersion
216
+ ? (releaseWithAppStoreVersionId ?? null)
217
+ : null,
218
+ }), {
219
+ pending: 'Updating App Clip default experience...',
220
+ success: 'Updated App Clip default experience',
221
+ failure: 'Failed updating App Clip default experience',
222
+ });
223
+ context.appClipDefaultExperience = experience;
224
+ }
225
+ else {
226
+ log_1.default.log((0, chalk_1.default) `{dim - Skipped App Clip default experience, no changes}`);
227
+ }
228
+ }
229
+ // Sync App Store review detail (invocation URLs).
230
+ const desiredReview = desired.reviewDetail;
231
+ if (desiredReview && desiredReview.invocationUrls.length > 0) {
232
+ if (context.appClipReviewDetail) {
233
+ const currentUrls = context.appClipReviewDetail.attributes.invocationUrls ?? [];
234
+ const desiredUrls = desiredReview.invocationUrls;
235
+ const unchanged = currentUrls.length === desiredUrls.length &&
236
+ currentUrls.every((url, i) => url === desiredUrls[i]);
237
+ if (!unchanged) {
238
+ const existingReview = context.appClipReviewDetail;
239
+ context.appClipReviewDetail = await (0, log_2.logAsync)(() => existingReview.updateAsync({
240
+ invocationUrls: desiredUrls,
241
+ }), {
242
+ pending: 'Updating App Clip review invocation URLs...',
243
+ success: 'Updated App Clip review invocation URLs',
244
+ failure: 'Failed updating App Clip review invocation URLs',
245
+ });
246
+ }
247
+ }
248
+ else {
249
+ context.appClipReviewDetail = await (0, log_2.logAsync)(() => apple_utils_1.AppClipAppStoreReviewDetail.createAsync(context.app.context, {
250
+ appClipDefaultExperienceId: experience.id,
251
+ attributes: { invocationUrls: desiredReview.invocationUrls },
252
+ }), {
253
+ pending: 'Creating App Clip review invocation URLs...',
254
+ success: 'Created App Clip review invocation URLs',
255
+ failure: 'Failed creating App Clip review invocation URLs',
256
+ });
257
+ }
258
+ }
259
+ else if (context.appClipReviewDetail) {
260
+ await (0, log_2.logAsync)(() => context.appClipReviewDetail.deleteAsync(), {
261
+ pending: 'Removing App Clip review invocation URLs...',
262
+ success: 'Removed App Clip review invocation URLs',
263
+ failure: 'Failed removing App Clip review invocation URLs',
264
+ });
265
+ context.appClipReviewDetail = null;
266
+ }
267
+ // Sync localizations: create/update from config, delete locales that
268
+ // exist in ASC but were removed from the config.
269
+ const desiredLocales = config.getAppClipLocales();
270
+ const desiredLocaleSet = new Set(desiredLocales);
271
+ for (const locale of desiredLocales) {
272
+ const desiredInfo = config.getAppClipLocalizedInfo(locale);
273
+ if (!desiredInfo) {
274
+ continue;
275
+ }
276
+ let localization = context.appClipLocalizations.get(locale);
277
+ if (!localization) {
278
+ localization = await (0, log_2.logAsync)(() => experience.createAppClipDefaultExperienceLocalizationAsync({
279
+ locale,
280
+ subtitle: desiredInfo.subtitle ?? null,
281
+ }), {
282
+ pending: `Creating App Clip localization for ${chalk_1.default.bold(locale)}...`,
283
+ success: `Created App Clip localization for ${chalk_1.default.bold(locale)}`,
284
+ failure: `Failed creating App Clip localization for ${chalk_1.default.bold(locale)}`,
285
+ });
286
+ context.appClipLocalizations.set(locale, localization);
287
+ }
288
+ else if (localization.attributes.subtitle !== (desiredInfo.subtitle ?? null)) {
289
+ localization = await (0, log_2.logAsync)(() => localization.updateAsync({ subtitle: desiredInfo.subtitle ?? null }), {
290
+ pending: `Updating App Clip localization for ${chalk_1.default.bold(locale)}...`,
291
+ success: `Updated App Clip localization for ${chalk_1.default.bold(locale)}`,
292
+ failure: `Failed updating App Clip localization for ${chalk_1.default.bold(locale)}`,
293
+ });
294
+ context.appClipLocalizations.set(locale, localization);
295
+ }
296
+ // Sync header image.
297
+ await syncAppClipHeaderImageAsync({
298
+ projectDir: context.projectDir,
299
+ localization,
300
+ existing: context.appClipHeaderImages.get(locale),
301
+ desiredPath: desiredInfo.headerImage,
302
+ onUploaded: image => context.appClipHeaderImages.set(locale, image),
303
+ onDeleted: () => context.appClipHeaderImages.delete(locale),
304
+ });
305
+ }
306
+ // Delete localizations no longer in config.
307
+ for (const [locale, localization] of context.appClipLocalizations) {
308
+ if (!desiredLocaleSet.has(locale)) {
309
+ await (0, log_2.logAsync)(() => localization.deleteAsync(), {
310
+ pending: `Deleting App Clip localization for ${chalk_1.default.bold(locale)}...`,
311
+ success: `Deleted App Clip localization for ${chalk_1.default.bold(locale)}`,
312
+ failure: `Failed deleting App Clip localization for ${chalk_1.default.bold(locale)}`,
313
+ });
314
+ context.appClipLocalizations.delete(locale);
315
+ context.appClipHeaderImages.delete(locale);
316
+ }
317
+ }
318
+ }
319
+ }
320
+ exports.AppClipTask = AppClipTask;
321
+ /**
322
+ * Upload, replace, or delete the header image for a single App Clip
323
+ * localization based on the desired config path.
324
+ */
325
+ async function syncAppClipHeaderImageAsync({ projectDir, localization, existing, desiredPath, onUploaded, onDeleted, }) {
326
+ const locale = localization.attributes.locale;
327
+ if (!desiredPath) {
328
+ if (existing) {
329
+ await (0, log_2.logAsync)(() => existing.deleteAsync(), {
330
+ pending: `Deleting App Clip header image for ${chalk_1.default.bold(locale)}...`,
331
+ success: `Deleted App Clip header image for ${chalk_1.default.bold(locale)}`,
332
+ failure: `Failed deleting App Clip header image for ${chalk_1.default.bold(locale)}`,
333
+ });
334
+ onDeleted();
335
+ }
336
+ return;
337
+ }
338
+ const absolutePath = path_1.default.resolve(projectDir, desiredPath);
339
+ if (!fs_1.default.existsSync(absolutePath)) {
340
+ log_1.default.warn((0, chalk_1.default) `{yellow App Clip header image not found: ${absolutePath}}`);
341
+ return;
342
+ }
343
+ const fileName = path_1.default.basename(absolutePath);
344
+ // Skip upload if the existing image already has the same filename and is
345
+ // either fully processed or still being processed (i.e. anything that
346
+ // isn't AWAITING_UPLOAD or FAILED). Apple does not expose the original
347
+ // source bytes (only re-rendered copies), so we can't reliably compare
348
+ // file size or checksum after a round-trip pull — filename is the only
349
+ // stable identity available.
350
+ if (existing &&
351
+ !existing.isAwaitingUpload() &&
352
+ !existing.isFailed() &&
353
+ existing.attributes.fileName === fileName) {
354
+ return;
355
+ }
356
+ if (existing) {
357
+ await (0, log_2.logAsync)(() => existing.deleteAsync(), {
358
+ pending: `Replacing App Clip header image for ${chalk_1.default.bold(locale)}...`,
359
+ success: `Removed previous App Clip header image for ${chalk_1.default.bold(locale)}`,
360
+ failure: `Failed removing previous App Clip header image for ${chalk_1.default.bold(locale)}`,
361
+ });
362
+ onDeleted();
363
+ }
364
+ const uploaded = await (0, log_2.logAsync)(() => apple_utils_1.AppClipHeaderImage.uploadAsync(localization.context, {
365
+ id: localization.id,
366
+ filePath: absolutePath,
367
+ waitForProcessing: true,
368
+ }), {
369
+ pending: `Uploading App Clip header image ${chalk_1.default.bold(fileName)} (${locale})...`,
370
+ success: `Uploaded App Clip header image ${chalk_1.default.bold(fileName)} (${locale})`,
371
+ failure: `Failed uploading App Clip header image ${chalk_1.default.bold(fileName)} (${locale})`,
372
+ });
373
+ onUploaded(uploaded);
374
+ }
375
+ /**
376
+ * Download an App Clip header image to the local filesystem.
377
+ * Returns the relative path to the downloaded file, or null on failure.
378
+ */
379
+ async function downloadAppClipHeaderImageAsync(projectDir, locale, headerImage) {
380
+ const imageUrl = headerImage.getImageAssetUrl({ type: 'png' });
381
+ if (!imageUrl) {
382
+ log_1.default.warn((0, chalk_1.default) `{yellow Could not get download URL for App Clip header image (${locale})}`);
383
+ return null;
384
+ }
385
+ const targetDir = path_1.default.join(projectDir, 'store', 'apple', 'app-clip', locale);
386
+ await fs_1.default.promises.mkdir(targetDir, { recursive: true });
387
+ const fileName = headerImage.attributes.fileName || 'header.png';
388
+ const outputPath = path_1.default.join(targetDir, fileName);
389
+ const relativePath = path_1.default.relative(projectDir, outputPath);
390
+ try {
391
+ const response = await (0, fetch_1.default)(imageUrl);
392
+ if (!response.ok) {
393
+ throw new Error(`HTTP ${response.status}`);
394
+ }
395
+ const buffer = await response.buffer();
396
+ await fs_1.default.promises.writeFile(outputPath, buffer);
397
+ log_1.default.log((0, chalk_1.default) `{dim Downloaded App Clip header image: ${relativePath}}`);
398
+ return relativePath;
399
+ }
400
+ catch (error) {
401
+ log_1.default.warn((0, chalk_1.default) `{yellow Failed to download App Clip header image (${locale}): ${error.message}}`);
402
+ return null;
403
+ }
404
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createAppleTasks = createAppleTasks;
4
4
  const age_rating_1 = require("./age-rating");
5
+ const app_clip_1 = require("./app-clip");
5
6
  const app_info_1 = require("./app-info");
6
7
  const app_review_detail_1 = require("./app-review-detail");
7
8
  const app_version_1 = require("./app-version");
@@ -18,5 +19,6 @@ function createAppleTasks({ version } = {}) {
18
19
  new app_review_detail_1.AppReviewDetailTask(),
19
20
  new screenshots_1.ScreenshotsTask(),
20
21
  new previews_1.PreviewsTask(),
22
+ new app_clip_1.AppClipTask(),
21
23
  ];
22
24
  }
@@ -60,18 +60,22 @@ class PreviewsTask extends task_1.AppleTask {
60
60
  // For now, we only handle the first preview per set (App Store allows up to 3)
61
61
  // We can extend this later to support multiple previews
62
62
  const preview = previewModels[0];
63
- const relativePath = await downloadPreviewAsync(context.projectDir, localeCode, previewType, preview, 0);
64
- if (relativePath) {
65
- // Include preview frame time code if available
66
- if (preview.attributes.previewFrameTimeCode) {
67
- previews[previewType] = {
68
- path: relativePath,
69
- previewFrameTimeCode: preview.attributes.previewFrameTimeCode,
70
- };
71
- }
72
- else {
73
- previews[previewType] = relativePath;
74
- }
63
+ const downloaded = await downloadPreviewAsync(context.projectDir, localeCode, previewType, preview, 0);
64
+ // When the download succeeds, write the real path. When it fails
65
+ // (e.g. the preview is in a broken AWAITING_UPLOAD state with no
66
+ // rendered videoUrl), preserve the entry in config so the user can
67
+ // either drop in a replacement file or remove the entry to delete
68
+ // the broken ASC record.
69
+ const fileName = preview.attributes.fileName || 'preview.mp4';
70
+ const relativePath = downloaded || path_1.default.join('store', 'apple', 'preview', localeCode, previewType, fileName);
71
+ if (preview.attributes.previewFrameTimeCode) {
72
+ previews[previewType] = {
73
+ path: relativePath,
74
+ previewFrameTimeCode: preview.attributes.previewFrameTimeCode,
75
+ };
76
+ }
77
+ else {
78
+ previews[previewType] = relativePath;
75
79
  }
76
80
  }
77
81
  if (Object.keys(previews).length > 0) {
@@ -48,14 +48,25 @@ class ScreenshotsTask extends task_1.AppleTask {
48
48
  if (!screenshotModels || screenshotModels.length === 0) {
49
49
  continue;
50
50
  }
51
- // Download screenshots and save to local filesystem
51
+ // Download screenshots and save to local filesystem. When a screenshot
52
+ // is in a broken state (AWAITING_UPLOAD with no rendered imageAsset)
53
+ // the download will fail, but we still preserve the entry pointing at
54
+ // its expected local path so users can either drop in a replacement
55
+ // file or remove the entry to delete the broken ASC record.
52
56
  const paths = [];
53
57
  for (let i = 0; i < screenshotModels.length; i++) {
54
58
  const screenshot = screenshotModels[i];
55
- const relativePath = await downloadScreenshotAsync(context.projectDir, localeCode, displayType, screenshot, i);
56
- if (relativePath) {
57
- paths.push(relativePath);
59
+ const downloaded = await downloadScreenshotAsync(context.projectDir, localeCode, displayType, screenshot, i);
60
+ if (downloaded) {
61
+ paths.push(downloaded);
62
+ continue;
58
63
  }
64
+ // Fall back to a placeholder path so the entry isn't lost from
65
+ // config. Push will detect that the existing screenshot isn't
66
+ // complete and either re-upload (if a local file exists at this
67
+ // path) or warn and skip (if it doesn't).
68
+ const fileName = screenshot.attributes.fileName || `${String(i + 1).padStart(2, '0')}.png`;
69
+ paths.push(path_1.default.join('store', 'apple', 'screenshot', localeCode, displayType, fileName));
59
70
  }
60
71
  if (paths.length > 0) {
61
72
  screenshots[displayType] = paths;
@@ -1,4 +1,4 @@
1
- import type { AgeRatingDeclarationProps, PreviewType, ScreenshotDisplayType } from '@expo/apple-utils';
1
+ import type { AgeRatingDeclarationProps, AppClipAction, PreviewType, ScreenshotDisplayType } from '@expo/apple-utils';
2
2
  export type AppleLocale = string;
3
3
  /** Screenshot display type enum values from App Store Connect API */
4
4
  export type AppleScreenshotDisplayType = `${ScreenshotDisplayType}`;
@@ -38,6 +38,33 @@ export interface AppleMetadata {
38
38
  /** @deprecated Use screenshots/previews in AppleInfo instead */
39
39
  preview?: Record<string, string[]>;
40
40
  review?: AppleReview;
41
+ /** App Clip metadata. Only applies to apps that ship an App Clip target. */
42
+ appClip?: AppleAppClip;
43
+ }
44
+ /** App Clip action enum values from App Store Connect API */
45
+ export type AppleAppClipAction = `${AppClipAction}`;
46
+ export interface AppleAppClip {
47
+ /** The default experience for this App Clip. There is exactly one per app. */
48
+ defaultExperience?: AppleAppClipDefaultExperience;
49
+ }
50
+ export interface AppleAppClipDefaultExperience {
51
+ /** Action button shown in the App Clip card. Defaults to OPEN if unset. */
52
+ action?: AppleAppClipAction;
53
+ /** Whether to release this default experience alongside the next App Store version. */
54
+ releaseWithAppStoreVersion?: boolean;
55
+ /** App Store review invocation URLs (used by App Review to launch the clip). */
56
+ reviewDetail?: AppleAppClipReviewDetail;
57
+ /** Per-locale subtitle and header image. */
58
+ info?: Record<AppleLocale, AppleAppClipLocalizedInfo>;
59
+ }
60
+ export interface AppleAppClipReviewDetail {
61
+ invocationUrls: string[];
62
+ }
63
+ export interface AppleAppClipLocalizedInfo {
64
+ /** Subtitle shown in the App Clip card. Apple limits this to 43 characters. */
65
+ subtitle?: string;
66
+ /** Relative path (from project root) to the App Clip header image PNG. */
67
+ headerImage?: string;
41
68
  }
42
69
  export type AppleAdvisory = Omit<Partial<AgeRatingDeclarationProps>, 'seventeenPlus' | 'gamblingAndContests'>;
43
70
  /** Apps can define up to two categories, or categories with up to two subcategories */
@@ -0,0 +1,27 @@
1
+ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
2
+ import { AppObserveEvent, AppObserveEventsOrderBy, AppObservePlatform, AppPlatform, PageInfo } from '../graphql/generated';
3
+ export declare enum EventsOrderPreset {
4
+ Slowest = "SLOWEST",
5
+ Fastest = "FASTEST",
6
+ Newest = "NEWEST",
7
+ Oldest = "OLDEST"
8
+ }
9
+ export declare function resolveOrderBy(input: string): AppObserveEventsOrderBy;
10
+ interface FetchObserveEventsOptions {
11
+ metricName: string;
12
+ orderBy: AppObserveEventsOrderBy;
13
+ limit: number;
14
+ after?: string;
15
+ startTime: string;
16
+ endTime: string;
17
+ platform?: AppObservePlatform;
18
+ appVersion?: string;
19
+ updateId?: string;
20
+ }
21
+ interface FetchObserveEventsResult {
22
+ events: AppObserveEvent[];
23
+ pageInfo: PageInfo;
24
+ }
25
+ export declare function fetchObserveEventsAsync(graphqlClient: ExpoGraphqlClient, appId: string, options: FetchObserveEventsOptions): Promise<FetchObserveEventsResult>;
26
+ export declare function fetchTotalEventCountAsync(graphqlClient: ExpoGraphqlClient, appId: string, metricName: string, platforms: AppPlatform[], startTime: string, endTime: string): Promise<number>;
27
+ export {};
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventsOrderPreset = void 0;
4
+ exports.resolveOrderBy = resolveOrderBy;
5
+ exports.fetchObserveEventsAsync = fetchObserveEventsAsync;
6
+ exports.fetchTotalEventCountAsync = fetchTotalEventCountAsync;
7
+ const generated_1 = require("../graphql/generated");
8
+ const ObserveQuery_1 = require("../graphql/queries/ObserveQuery");
9
+ var EventsOrderPreset;
10
+ (function (EventsOrderPreset) {
11
+ EventsOrderPreset["Slowest"] = "SLOWEST";
12
+ EventsOrderPreset["Fastest"] = "FASTEST";
13
+ EventsOrderPreset["Newest"] = "NEWEST";
14
+ EventsOrderPreset["Oldest"] = "OLDEST";
15
+ })(EventsOrderPreset || (exports.EventsOrderPreset = EventsOrderPreset = {}));
16
+ function resolveOrderBy(input) {
17
+ const preset = input.toUpperCase();
18
+ switch (preset) {
19
+ case EventsOrderPreset.Slowest:
20
+ return {
21
+ field: generated_1.AppObserveEventsOrderByField.MetricValue,
22
+ direction: generated_1.AppObserveEventsOrderByDirection.Desc,
23
+ };
24
+ case EventsOrderPreset.Fastest:
25
+ return {
26
+ field: generated_1.AppObserveEventsOrderByField.MetricValue,
27
+ direction: generated_1.AppObserveEventsOrderByDirection.Asc,
28
+ };
29
+ case EventsOrderPreset.Newest:
30
+ return {
31
+ field: generated_1.AppObserveEventsOrderByField.Timestamp,
32
+ direction: generated_1.AppObserveEventsOrderByDirection.Desc,
33
+ };
34
+ case EventsOrderPreset.Oldest:
35
+ return {
36
+ field: generated_1.AppObserveEventsOrderByField.Timestamp,
37
+ direction: generated_1.AppObserveEventsOrderByDirection.Asc,
38
+ };
39
+ }
40
+ }
41
+ async function fetchObserveEventsAsync(graphqlClient, appId, options) {
42
+ const filter = {
43
+ metricName: options.metricName,
44
+ startTime: options.startTime,
45
+ endTime: options.endTime,
46
+ ...(options.platform && { platform: options.platform }),
47
+ ...(options.appVersion && { appVersion: options.appVersion }),
48
+ ...(options.updateId && { appUpdateId: options.updateId }),
49
+ };
50
+ return await ObserveQuery_1.ObserveQuery.eventsAsync(graphqlClient, {
51
+ appId,
52
+ filter,
53
+ first: options.limit,
54
+ ...(options.after && { after: options.after }),
55
+ orderBy: options.orderBy,
56
+ });
57
+ }
58
+ const appPlatformToObservePlatform = {
59
+ [generated_1.AppPlatform.Android]: generated_1.AppObservePlatform.Android,
60
+ [generated_1.AppPlatform.Ios]: generated_1.AppObservePlatform.Ios,
61
+ };
62
+ async function fetchTotalEventCountAsync(graphqlClient, appId, metricName, platforms, startTime, endTime) {
63
+ const queries = platforms.map(async (appPlatform) => {
64
+ try {
65
+ const versions = await ObserveQuery_1.ObserveQuery.appVersionsAsync(graphqlClient, {
66
+ appId,
67
+ platform: appPlatformToObservePlatform[appPlatform],
68
+ startTime,
69
+ endTime,
70
+ metricNames: [metricName],
71
+ });
72
+ return versions.reduce((sum, v) => {
73
+ const metric = v.metrics.find(m => m.metricName === metricName);
74
+ return sum + (metric?.eventCount ?? 0);
75
+ }, 0);
76
+ }
77
+ catch {
78
+ return 0;
79
+ }
80
+ });
81
+ const counts = await Promise.all(queries);
82
+ return counts.reduce((a, b) => a + b, 0);
83
+ }
@@ -0,0 +1,11 @@
1
+ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
2
+ import { AppPlatform } from '../graphql/generated';
3
+ import { BuildNumbersMap, ObserveMetricsMap, UpdateIdsMap } from './formatMetrics';
4
+ export declare function validateDateFlag(value: string, flagName: string): void;
5
+ export interface FetchObserveMetricsResult {
6
+ metricsMap: ObserveMetricsMap;
7
+ buildNumbersMap: BuildNumbersMap;
8
+ updateIdsMap: UpdateIdsMap;
9
+ totalEventCounts: Map<string, number>;
10
+ }
11
+ export declare function fetchObserveMetricsAsync(graphqlClient: ExpoGraphqlClient, appId: string, metricNames: string[], platforms: AppPlatform[], startTime: string, endTime: string): Promise<FetchObserveMetricsResult>;