eas-cli 18.3.0 → 18.5.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 (58) hide show
  1. package/README.md +98 -95
  2. package/build/build/android/prepareJob.js +2 -2
  3. package/build/build/build.js +27 -6
  4. package/build/build/ios/prepareJob.js +2 -2
  5. package/build/build/metadata.js +2 -1
  6. package/build/commandUtils/EasCommand.js +23 -2
  7. package/build/commandUtils/context/contextUtils/getProjectIdAsync.js +2 -0
  8. package/build/commandUtils/flags.d.ts +1 -0
  9. package/build/commandUtils/flags.js +12 -0
  10. package/build/commandUtils/workflow/fetchLogs.js +11 -2
  11. package/build/commandUtils/workflow/types.d.ts +5 -1
  12. package/build/commandUtils/workflow/utils.js +22 -16
  13. package/build/commands/build/dev.d.ts +1 -0
  14. package/build/commands/build/dev.js +9 -1
  15. package/build/commands/metadata/pull.d.ts +1 -0
  16. package/build/commands/metadata/pull.js +11 -4
  17. package/build/commands/metadata/push.d.ts +1 -0
  18. package/build/commands/metadata/push.js +11 -4
  19. package/build/commands/project/onboarding.js +3 -0
  20. package/build/commands/workflow/logs.js +12 -12
  21. package/build/graphql/generated.d.ts +673 -46
  22. package/build/graphql/generated.js +58 -20
  23. package/build/graphql/queries/UserQuery.js +3 -0
  24. package/build/graphql/types/Update.js +3 -0
  25. package/build/metadata/apple/config/reader.d.ts +5 -1
  26. package/build/metadata/apple/config/reader.js +8 -0
  27. package/build/metadata/apple/config/writer.d.ts +5 -1
  28. package/build/metadata/apple/config/writer.js +13 -0
  29. package/build/metadata/apple/data.d.ts +6 -2
  30. package/build/metadata/apple/rules/infoRestrictedWords.js +6 -1
  31. package/build/metadata/apple/tasks/age-rating.d.ts +1 -1
  32. package/build/metadata/apple/tasks/age-rating.js +19 -3
  33. package/build/metadata/apple/tasks/app-review-detail.js +7 -2
  34. package/build/metadata/apple/tasks/index.js +4 -0
  35. package/build/metadata/apple/tasks/previews.d.ts +18 -0
  36. package/build/metadata/apple/tasks/previews.js +208 -0
  37. package/build/metadata/apple/tasks/screenshots.d.ts +18 -0
  38. package/build/metadata/apple/tasks/screenshots.js +224 -0
  39. package/build/metadata/apple/types.d.ts +34 -1
  40. package/build/metadata/auth.d.ts +11 -1
  41. package/build/metadata/auth.js +96 -2
  42. package/build/metadata/download.d.ts +5 -1
  43. package/build/metadata/download.js +16 -8
  44. package/build/metadata/upload.d.ts +5 -1
  45. package/build/metadata/upload.js +11 -4
  46. package/build/project/projectUtils.d.ts +0 -2
  47. package/build/project/projectUtils.js +0 -12
  48. package/build/project/workflow.js +1 -1
  49. package/build/sentry.d.ts +2 -0
  50. package/build/sentry.js +22 -0
  51. package/build/update/utils.d.ts +2 -2
  52. package/build/update/utils.js +1 -0
  53. package/build/user/User.d.ts +2 -2
  54. package/build/user/User.js +3 -0
  55. package/build/user/expoBrowserAuthFlowLauncher.js +70 -13
  56. package/oclif.manifest.json +1995 -1918
  57. package/package.json +12 -11
  58. package/schema/metadata-0.json +36 -0
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PreviewsTask = 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
+ * Normalize preview config to always return an object with path and optional previewFrameTimeCode.
15
+ */
16
+ function normalizePreviewConfig(config) {
17
+ if (typeof config === 'string') {
18
+ return { path: config };
19
+ }
20
+ return config;
21
+ }
22
+ /**
23
+ * Task for managing App Store video previews.
24
+ * Downloads existing previews and uploads new ones based on store configuration.
25
+ */
26
+ class PreviewsTask extends task_1.AppleTask {
27
+ name = () => 'video previews';
28
+ async prepareAsync({ context }) {
29
+ // Initialize the preview sets map
30
+ context.previewSets = new Map();
31
+ if (!context.versionLocales) {
32
+ return;
33
+ }
34
+ // Fetch preview sets for all locales in parallel
35
+ await Promise.all(context.versionLocales.map(async (locale) => {
36
+ const sets = await locale.getAppPreviewSetsAsync();
37
+ const previewTypeMap = new Map();
38
+ for (const set of sets) {
39
+ previewTypeMap.set(set.attributes.previewType, set);
40
+ }
41
+ context.previewSets.set(locale.attributes.locale, previewTypeMap);
42
+ }));
43
+ }
44
+ async downloadAsync({ config, context }) {
45
+ if (!context.previewSets || !context.versionLocales) {
46
+ return;
47
+ }
48
+ for (const locale of context.versionLocales) {
49
+ const localeCode = locale.attributes.locale;
50
+ const previewTypeMap = context.previewSets.get(localeCode);
51
+ if (!previewTypeMap || previewTypeMap.size === 0) {
52
+ continue;
53
+ }
54
+ const previews = {};
55
+ for (const [previewType, set] of previewTypeMap) {
56
+ const previewModels = set.attributes.appPreviews;
57
+ if (!previewModels || previewModels.length === 0) {
58
+ continue;
59
+ }
60
+ // For now, we only handle the first preview per set (App Store allows up to 3)
61
+ // We can extend this later to support multiple previews
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
+ }
75
+ }
76
+ }
77
+ if (Object.keys(previews).length > 0) {
78
+ config.setPreviews(localeCode, previews);
79
+ }
80
+ }
81
+ }
82
+ async uploadAsync({ config, context }) {
83
+ if (!context.previewSets || !context.versionLocales) {
84
+ log_1.default.log((0, chalk_1.default) `{dim - Skipped video previews, no version available}`);
85
+ return;
86
+ }
87
+ const locales = config.getLocales();
88
+ if (locales.length <= 0) {
89
+ log_1.default.log((0, chalk_1.default) `{dim - Skipped video previews, no locales configured}`);
90
+ return;
91
+ }
92
+ for (const localeCode of locales) {
93
+ const previews = config.getPreviews(localeCode);
94
+ if (!previews || Object.keys(previews).length === 0) {
95
+ continue;
96
+ }
97
+ const localization = context.versionLocales.find(l => l.attributes.locale === localeCode);
98
+ if (!localization) {
99
+ log_1.default.warn((0, chalk_1.default) `{yellow Skipping video previews for ${localeCode} - locale not found}`);
100
+ continue;
101
+ }
102
+ for (const [previewType, previewConfig] of Object.entries(previews)) {
103
+ if (!previewConfig) {
104
+ continue;
105
+ }
106
+ await syncPreviewSetAsync(context.projectDir, localization, previewType, normalizePreviewConfig(previewConfig), context.previewSets.get(localeCode));
107
+ }
108
+ }
109
+ }
110
+ }
111
+ exports.PreviewsTask = PreviewsTask;
112
+ /**
113
+ * Sync a preview set - upload new preview, delete old one if changed.
114
+ */
115
+ async function syncPreviewSetAsync(projectDir, localization, previewType, previewConfig, existingSets) {
116
+ const locale = localization.attributes.locale;
117
+ const absolutePath = path_1.default.resolve(projectDir, previewConfig.path);
118
+ const fileName = path_1.default.basename(absolutePath);
119
+ if (!fs_1.default.existsSync(absolutePath)) {
120
+ log_1.default.warn((0, chalk_1.default) `{yellow Video preview not found: ${absolutePath}}`);
121
+ return;
122
+ }
123
+ // Get or create the preview set
124
+ let previewSet = existingSets?.get(previewType);
125
+ if (!previewSet) {
126
+ previewSet = await (0, log_2.logAsync)(() => localization.createAppPreviewSetAsync({
127
+ previewType,
128
+ }), {
129
+ pending: `Creating preview set for ${chalk_1.default.bold(previewType)} (${locale})...`,
130
+ success: `Created preview set for ${chalk_1.default.bold(previewType)} (${locale})`,
131
+ failure: `Failed creating preview set for ${chalk_1.default.bold(previewType)} (${locale})`,
132
+ });
133
+ }
134
+ const existingPreviews = previewSet.attributes.appPreviews || [];
135
+ // Check if we need to update (different filename, size, or no existing preview)
136
+ const existingPreview = existingPreviews.find(p => p.attributes.fileName === fileName);
137
+ const localSize = fs_1.default.statSync(absolutePath).size;
138
+ if (existingPreview &&
139
+ existingPreview.isComplete() &&
140
+ existingPreview.attributes.fileSize === localSize) {
141
+ // Preview with same filename exists, check if we need to update preview frame time code
142
+ if (previewConfig.previewFrameTimeCode &&
143
+ existingPreview.attributes.previewFrameTimeCode !== previewConfig.previewFrameTimeCode) {
144
+ await (0, log_2.logAsync)(() => existingPreview.updateAsync({
145
+ previewFrameTimeCode: previewConfig.previewFrameTimeCode,
146
+ }), {
147
+ pending: `Updating preview frame time code for ${chalk_1.default.bold(fileName)} (${locale})...`,
148
+ success: `Updated preview frame time code for ${chalk_1.default.bold(fileName)} (${locale})`,
149
+ failure: `Failed updating preview frame time code for ${chalk_1.default.bold(fileName)} (${locale})`,
150
+ });
151
+ }
152
+ log_1.default.log((0, chalk_1.default) `{dim Preview ${fileName} already exists, skipping upload}`);
153
+ return;
154
+ }
155
+ // Delete existing previews that don't match
156
+ for (const preview of existingPreviews) {
157
+ if (preview.attributes.fileName !== fileName) {
158
+ await (0, log_2.logAsync)(() => preview.deleteAsync(), {
159
+ pending: `Deleting old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})...`,
160
+ success: `Deleted old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})`,
161
+ failure: `Failed deleting old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})`,
162
+ });
163
+ }
164
+ }
165
+ // Upload new preview
166
+ await (0, log_2.logAsync)(() => apple_utils_1.AppPreview.uploadAsync(localization.context, {
167
+ id: previewSet.id,
168
+ filePath: absolutePath,
169
+ waitForProcessing: true,
170
+ previewFrameTimeCode: previewConfig.previewFrameTimeCode,
171
+ }), {
172
+ pending: `Uploading video preview ${chalk_1.default.bold(fileName)} (${locale})...`,
173
+ success: `Uploaded video preview ${chalk_1.default.bold(fileName)} (${locale})`,
174
+ failure: `Failed uploading video preview ${chalk_1.default.bold(fileName)} (${locale})`,
175
+ });
176
+ }
177
+ /**
178
+ * Download a video preview to the local filesystem.
179
+ * Returns the relative path to the downloaded file.
180
+ */
181
+ async function downloadPreviewAsync(projectDir, locale, previewType, preview, index) {
182
+ const videoUrl = preview.getVideoUrl();
183
+ if (!videoUrl) {
184
+ log_1.default.warn((0, chalk_1.default) `{yellow Could not get download URL for preview ${preview.attributes.fileName}}`);
185
+ return null;
186
+ }
187
+ // Create directory structure: store/apple/preview/{locale}/{previewType}/
188
+ const previewsDir = path_1.default.join(projectDir, 'store', 'apple', 'preview', locale, previewType);
189
+ await fs_1.default.promises.mkdir(previewsDir, { recursive: true });
190
+ // Use original filename for matching during sync
191
+ const fileName = preview.attributes.fileName || `${String(index + 1).padStart(2, '0')}.mp4`;
192
+ const outputPath = path_1.default.join(previewsDir, fileName);
193
+ const relativePath = path_1.default.relative(projectDir, outputPath);
194
+ try {
195
+ const response = await (0, fetch_1.default)(videoUrl);
196
+ if (!response.ok) {
197
+ throw new Error(`HTTP ${response.status}`);
198
+ }
199
+ const buffer = await response.buffer();
200
+ await fs_1.default.promises.writeFile(outputPath, buffer);
201
+ log_1.default.log((0, chalk_1.default) `{dim Downloaded video preview: ${relativePath}}`);
202
+ return relativePath;
203
+ }
204
+ catch (error) {
205
+ log_1.default.warn((0, chalk_1.default) `{yellow Failed to download video preview ${fileName}: ${error.message}}`);
206
+ return null;
207
+ }
208
+ }
@@ -0,0 +1,18 @@
1
+ import { AppScreenshotSet, ScreenshotDisplayType } from '@expo/apple-utils';
2
+ import { AppleTask, TaskDownloadOptions, TaskPrepareOptions, TaskUploadOptions } from '../task';
3
+ /** Locale -> ScreenshotDisplayType -> AppScreenshotSet */
4
+ export type ScreenshotSetsMap = Map<string, Map<ScreenshotDisplayType, AppScreenshotSet>>;
5
+ export type ScreenshotsData = {
6
+ /** Map of locales to their screenshot sets */
7
+ screenshotSets: ScreenshotSetsMap;
8
+ };
9
+ /**
10
+ * Task for managing App Store screenshots.
11
+ * Downloads existing screenshots and uploads new ones based on store configuration.
12
+ */
13
+ export declare class ScreenshotsTask extends AppleTask {
14
+ name: () => string;
15
+ prepareAsync({ context }: TaskPrepareOptions): Promise<void>;
16
+ downloadAsync({ config, context }: TaskDownloadOptions): Promise<void>;
17
+ uploadAsync({ config, context }: TaskUploadOptions): Promise<void>;
18
+ }
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ScreenshotsTask = 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 Store screenshots.
15
+ * Downloads existing screenshots and uploads new ones based on store configuration.
16
+ */
17
+ class ScreenshotsTask extends task_1.AppleTask {
18
+ name = () => 'screenshots';
19
+ async prepareAsync({ context }) {
20
+ // Initialize the screenshot sets map
21
+ context.screenshotSets = new Map();
22
+ if (!context.versionLocales) {
23
+ return;
24
+ }
25
+ // Fetch screenshot sets for all locales in parallel
26
+ await Promise.all(context.versionLocales.map(async (locale) => {
27
+ const sets = await locale.getAppScreenshotSetsAsync();
28
+ const displayTypeMap = new Map();
29
+ for (const set of sets) {
30
+ displayTypeMap.set(set.attributes.screenshotDisplayType, set);
31
+ }
32
+ context.screenshotSets.set(locale.attributes.locale, displayTypeMap);
33
+ }));
34
+ }
35
+ async downloadAsync({ config, context }) {
36
+ if (!context.screenshotSets || !context.versionLocales) {
37
+ return;
38
+ }
39
+ for (const locale of context.versionLocales) {
40
+ const localeCode = locale.attributes.locale;
41
+ const displayTypeMap = context.screenshotSets.get(localeCode);
42
+ if (!displayTypeMap || displayTypeMap.size === 0) {
43
+ continue;
44
+ }
45
+ const screenshots = {};
46
+ for (const [displayType, set] of displayTypeMap) {
47
+ const screenshotModels = set.attributes.appScreenshots;
48
+ if (!screenshotModels || screenshotModels.length === 0) {
49
+ continue;
50
+ }
51
+ // Download screenshots and save to local filesystem
52
+ const paths = [];
53
+ for (let i = 0; i < screenshotModels.length; i++) {
54
+ const screenshot = screenshotModels[i];
55
+ const relativePath = await downloadScreenshotAsync(context.projectDir, localeCode, displayType, screenshot, i);
56
+ if (relativePath) {
57
+ paths.push(relativePath);
58
+ }
59
+ }
60
+ if (paths.length > 0) {
61
+ screenshots[displayType] = paths;
62
+ }
63
+ }
64
+ if (Object.keys(screenshots).length > 0) {
65
+ config.setScreenshots(localeCode, screenshots);
66
+ }
67
+ }
68
+ }
69
+ async uploadAsync({ config, context }) {
70
+ if (!context.screenshotSets || !context.versionLocales) {
71
+ log_1.default.log((0, chalk_1.default) `{dim - Skipped screenshots, no version available}`);
72
+ return;
73
+ }
74
+ const locales = config.getLocales();
75
+ if (locales.length <= 0) {
76
+ log_1.default.log((0, chalk_1.default) `{dim - Skipped screenshots, no locales configured}`);
77
+ return;
78
+ }
79
+ for (const localeCode of locales) {
80
+ const screenshots = config.getScreenshots(localeCode);
81
+ if (!screenshots || Object.keys(screenshots).length === 0) {
82
+ continue;
83
+ }
84
+ const localization = context.versionLocales.find(l => l.attributes.locale === localeCode);
85
+ if (!localization) {
86
+ log_1.default.warn((0, chalk_1.default) `{yellow Skipping screenshots for ${localeCode} - locale not found}`);
87
+ continue;
88
+ }
89
+ for (const [displayType, paths] of Object.entries(screenshots)) {
90
+ if (!paths || paths.length === 0) {
91
+ continue;
92
+ }
93
+ await syncScreenshotSetAsync(context.projectDir, localization, displayType, paths, context.screenshotSets.get(localeCode));
94
+ }
95
+ }
96
+ }
97
+ }
98
+ exports.ScreenshotsTask = ScreenshotsTask;
99
+ /**
100
+ * Sync a screenshot set - upload new screenshots, delete removed ones, reorder if needed.
101
+ */
102
+ async function syncScreenshotSetAsync(projectDir, localization, displayType, paths, existingSets) {
103
+ const locale = localization.attributes.locale;
104
+ // Get or create the screenshot set
105
+ let screenshotSet = existingSets?.get(displayType);
106
+ if (!screenshotSet) {
107
+ screenshotSet = await (0, log_2.logAsync)(() => localization.createAppScreenshotSetAsync({
108
+ screenshotDisplayType: displayType,
109
+ }), {
110
+ pending: `Creating screenshot set for ${chalk_1.default.bold(displayType)} (${locale})...`,
111
+ success: `Created screenshot set for ${chalk_1.default.bold(displayType)} (${locale})`,
112
+ failure: `Failed creating screenshot set for ${chalk_1.default.bold(displayType)} (${locale})`,
113
+ });
114
+ }
115
+ const existingScreenshots = screenshotSet.attributes.appScreenshots || [];
116
+ // Build a map of existing screenshots by filename for comparison
117
+ const existingByFilename = new Map();
118
+ for (const screenshot of existingScreenshots) {
119
+ existingByFilename.set(screenshot.attributes.fileName, screenshot);
120
+ }
121
+ // Track which screenshots to keep, upload, and delete
122
+ const screenshotIdsToKeep = [];
123
+ const pathsToUpload = [];
124
+ for (const relativePath of paths) {
125
+ const absolutePath = path_1.default.resolve(projectDir, relativePath);
126
+ const fileName = path_1.default.basename(absolutePath);
127
+ // Check if screenshot already exists with same name and file size
128
+ const existing = existingByFilename.get(fileName);
129
+ const localSize = fs_1.default.existsSync(absolutePath) ? fs_1.default.statSync(absolutePath).size : null;
130
+ if (existing &&
131
+ existing.isComplete() &&
132
+ (localSize === null || existing.attributes.fileSize === localSize)) {
133
+ screenshotIdsToKeep.push(existing.id);
134
+ existingByFilename.delete(fileName);
135
+ }
136
+ else {
137
+ pathsToUpload.push(absolutePath);
138
+ }
139
+ }
140
+ // Delete screenshots that are no longer in config
141
+ for (const screenshot of existingByFilename.values()) {
142
+ await (0, log_2.logAsync)(() => screenshot.deleteAsync(), {
143
+ pending: `Deleting screenshot ${chalk_1.default.bold(screenshot.attributes.fileName)} (${locale})...`,
144
+ success: `Deleted screenshot ${chalk_1.default.bold(screenshot.attributes.fileName)} (${locale})`,
145
+ failure: `Failed deleting screenshot ${chalk_1.default.bold(screenshot.attributes.fileName)} (${locale})`,
146
+ });
147
+ }
148
+ // Upload new screenshots
149
+ for (const absolutePath of pathsToUpload) {
150
+ const fileName = path_1.default.basename(absolutePath);
151
+ if (!fs_1.default.existsSync(absolutePath)) {
152
+ log_1.default.warn((0, chalk_1.default) `{yellow Screenshot not found: ${absolutePath}}`);
153
+ continue;
154
+ }
155
+ const newScreenshot = await (0, log_2.logAsync)(() => apple_utils_1.AppScreenshot.uploadAsync(localization.context, {
156
+ id: screenshotSet.id,
157
+ filePath: absolutePath,
158
+ waitForProcessing: true,
159
+ }), {
160
+ pending: `Uploading screenshot ${chalk_1.default.bold(fileName)} (${locale})...`,
161
+ success: `Uploaded screenshot ${chalk_1.default.bold(fileName)} (${locale})`,
162
+ failure: `Failed uploading screenshot ${chalk_1.default.bold(fileName)} (${locale})`,
163
+ });
164
+ screenshotIdsToKeep.push(newScreenshot.id);
165
+ }
166
+ // Reorder screenshots to match config order
167
+ if (screenshotIdsToKeep.length > 0) {
168
+ const refreshedSet = await apple_utils_1.AppScreenshotSet.infoAsync(localization.context, {
169
+ id: screenshotSet.id,
170
+ });
171
+ const refreshedScreenshots = refreshedSet.attributes.appScreenshots || [];
172
+ const screenshotsByFilename = new Map();
173
+ for (const s of refreshedScreenshots) {
174
+ screenshotsByFilename.set(s.attributes.fileName, s);
175
+ }
176
+ // Build the desired order based on config paths
177
+ const orderedIds = [];
178
+ for (const relativePath of paths) {
179
+ const fileName = path_1.default.basename(relativePath);
180
+ const screenshot = screenshotsByFilename.get(fileName);
181
+ if (screenshot) {
182
+ orderedIds.push(screenshot.id);
183
+ }
184
+ }
185
+ // Only call reorder if the order actually differs from current
186
+ const currentIds = refreshedScreenshots.map(s => s.id);
187
+ if (orderedIds.length > 0 &&
188
+ (orderedIds.length !== currentIds.length || orderedIds.some((id, i) => id !== currentIds[i]))) {
189
+ await screenshotSet.reorderScreenshotsAsync({ appScreenshots: orderedIds });
190
+ }
191
+ }
192
+ }
193
+ /**
194
+ * Download a screenshot to the local filesystem.
195
+ * Returns the relative path to the downloaded file.
196
+ */
197
+ async function downloadScreenshotAsync(projectDir, locale, displayType, screenshot, index) {
198
+ const imageUrl = screenshot.getImageAssetUrl({ type: 'png' });
199
+ if (!imageUrl) {
200
+ log_1.default.warn((0, chalk_1.default) `{yellow Could not get download URL for screenshot ${screenshot.attributes.fileName}}`);
201
+ return null;
202
+ }
203
+ // Create directory structure: store/apple/screenshot/{locale}/{displayType}/
204
+ const screenshotsDir = path_1.default.join(projectDir, 'store', 'apple', 'screenshot', locale, displayType);
205
+ await fs_1.default.promises.mkdir(screenshotsDir, { recursive: true });
206
+ // Use original filename for matching during sync
207
+ const fileName = screenshot.attributes.fileName || `${String(index + 1).padStart(2, '0')}.png`;
208
+ const outputPath = path_1.default.join(screenshotsDir, fileName);
209
+ const relativePath = path_1.default.relative(projectDir, outputPath);
210
+ try {
211
+ const response = await (0, fetch_1.default)(imageUrl);
212
+ if (!response.ok) {
213
+ throw new Error(`HTTP ${response.status}`);
214
+ }
215
+ const buffer = await response.buffer();
216
+ await fs_1.default.promises.writeFile(outputPath, buffer);
217
+ log_1.default.log((0, chalk_1.default) `{dim Downloaded screenshot: ${relativePath}}`);
218
+ return relativePath;
219
+ }
220
+ catch (error) {
221
+ log_1.default.warn((0, chalk_1.default) `{yellow Failed to download screenshot ${fileName}: ${error.message}}`);
222
+ return null;
223
+ }
224
+ }
@@ -1,5 +1,33 @@
1
- import type { AgeRatingDeclarationProps } from '@expo/apple-utils';
1
+ import type { AgeRatingDeclarationProps, PreviewType, ScreenshotDisplayType } from '@expo/apple-utils';
2
2
  export type AppleLocale = string;
3
+ /** Screenshot display type enum values from App Store Connect API */
4
+ export type AppleScreenshotDisplayType = `${ScreenshotDisplayType}`;
5
+ /** Preview display type enum values from App Store Connect API */
6
+ export type ApplePreviewType = `${PreviewType}`;
7
+ /**
8
+ * Screenshots organized by display type.
9
+ * Key is the display type (e.g., 'APP_IPHONE_67'), value is array of file paths.
10
+ * @example { "APP_IPHONE_67": ["./screenshots/home.png", "./screenshots/profile.png"] }
11
+ */
12
+ export type AppleScreenshots = Partial<Record<AppleScreenshotDisplayType, string[]>>;
13
+ /**
14
+ * Video preview configuration - either a simple path string or an object with options.
15
+ * @example "./previews/demo.mp4"
16
+ * @example { path: "./previews/demo.mp4", previewFrameTimeCode: "00:05:00" }
17
+ */
18
+ export type ApplePreviewConfig = string | {
19
+ /** Video file path (relative to project root) */
20
+ path: string;
21
+ /** Optional preview frame time code (e.g., '00:05:00' for 5 seconds) */
22
+ previewFrameTimeCode?: string;
23
+ };
24
+ /**
25
+ * Video previews organized by display type.
26
+ * Key is the display type (e.g., 'IPHONE_67'), value is the preview config.
27
+ * @example { "IPHONE_67": "./previews/demo.mp4" }
28
+ * @example { "IPHONE_67": { path: "./previews/demo.mp4", previewFrameTimeCode: "00:05:00" } }
29
+ */
30
+ export type ApplePreviews = Partial<Record<ApplePreviewType, ApplePreviewConfig>>;
3
31
  export interface AppleMetadata {
4
32
  version?: string;
5
33
  copyright?: string;
@@ -7,6 +35,7 @@ export interface AppleMetadata {
7
35
  categories?: AppleCategory;
8
36
  release?: AppleRelease;
9
37
  advisory?: AppleAdvisory;
38
+ /** @deprecated Use screenshots/previews in AppleInfo instead */
10
39
  preview?: Record<string, string[]>;
11
40
  review?: AppleReview;
12
41
  }
@@ -30,6 +59,10 @@ export interface AppleInfo {
30
59
  privacyPolicyText?: string;
31
60
  privacyChoicesUrl?: string;
32
61
  supportUrl?: string;
62
+ /** Screenshots for this locale, organized by display type */
63
+ screenshots?: AppleScreenshots;
64
+ /** Video previews for this locale, organized by display type */
65
+ previews?: ApplePreviews;
33
66
  }
34
67
  export interface AppleReview {
35
68
  firstName: string;
@@ -1,6 +1,7 @@
1
1
  import { App, Session } from '@expo/apple-utils';
2
2
  import { ExpoConfig } from '@expo/config';
3
3
  import { SubmitProfile } from '@expo/eas-json';
4
+ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
4
5
  import { CredentialsContext } from '../credentials/context';
5
6
  export type MetadataAppStoreAuthentication = {
6
7
  /** The root entity of the App store */
@@ -11,10 +12,19 @@ export type MetadataAppStoreAuthentication = {
11
12
  /**
12
13
  * To start syncing ASC entities, we need access to the apple utils App instance.
13
14
  * This resolves both the authentication and that App instance.
15
+ *
16
+ * Resolution order for authentication:
17
+ * 1. ASC API key from environment variables (EXPO_ASC_API_KEY_PATH, etc.)
18
+ * 2. ASC API key from submit profile (ascApiKeyPath, etc. in eas.json)
19
+ * 3. ASC API key from EAS credentials service
20
+ * 4. Interactive cookie auth (only when not in non-interactive mode)
14
21
  */
15
- export declare function getAppStoreAuthAsync({ projectDir, profile, exp, credentialsCtx, }: {
22
+ export declare function getAppStoreAuthAsync({ projectDir, profile, exp, credentialsCtx, nonInteractive, graphqlClient, projectId, }: {
16
23
  projectDir: string;
17
24
  profile: SubmitProfile;
18
25
  exp: ExpoConfig;
19
26
  credentialsCtx: CredentialsContext;
27
+ nonInteractive: boolean;
28
+ graphqlClient: ExpoGraphqlClient;
29
+ projectId: string;
20
30
  }): Promise<MetadataAppStoreAuthentication>;
@@ -4,8 +4,14 @@ exports.getAppStoreAuthAsync = getAppStoreAuthAsync;
4
4
  const tslib_1 = require("tslib");
5
5
  const apple_utils_1 = require("@expo/apple-utils");
6
6
  const assert_1 = tslib_1.__importDefault(require("assert"));
7
+ const fs_1 = tslib_1.__importDefault(require("fs"));
7
8
  const authenticate_1 = require("../credentials/ios/appstore/authenticate");
9
+ const authenticateTypes_1 = require("../credentials/ios/appstore/authenticateTypes");
10
+ const resolveCredentials_1 = require("../credentials/ios/appstore/resolveCredentials");
11
+ const AppStoreConnectApiKeyQuery_1 = require("../graphql/queries/AppStoreConnectApiKeyQuery");
12
+ const log_1 = tslib_1.__importDefault(require("../log"));
8
13
  const bundleIdentifier_1 = require("../project/ios/bundleIdentifier");
14
+ const projectUtils_1 = require("../project/projectUtils");
9
15
  /**
10
16
  * Resolve the bundle identifier from the selected submit profile.
11
17
  * This bundle identifier is used as target for the metadata submission.
@@ -16,15 +22,103 @@ async function resolveAppStoreBundleIdentifierAsync(projectDir, profile, exp, vc
16
22
  }
17
23
  return await (0, bundleIdentifier_1.getBundleIdentifierAsync)(projectDir, exp, vcsClient);
18
24
  }
25
+ /**
26
+ * Try to resolve an ASC API key from the submit profile or EAS credentials service.
27
+ * Returns null if no key is available from these sources.
28
+ */
29
+ async function tryResolveAscApiKeyAsync({ profile, graphqlClient, projectId, exp, bundleId, }) {
30
+ // 1. Check submit profile for ASC API key fields
31
+ if ('ascApiKeyPath' in profile && 'ascApiKeyIssuerId' in profile && 'ascApiKeyId' in profile) {
32
+ const { ascApiKeyPath, ascApiKeyIssuerId, ascApiKeyId } = profile;
33
+ if (ascApiKeyPath && ascApiKeyIssuerId && ascApiKeyId) {
34
+ const keyP8 = await fs_1.default.promises.readFile(ascApiKeyPath, 'utf-8');
35
+ // Also try to get teamId from the profile if available
36
+ const teamId = 'appleTeamId' in profile ? profile.appleTeamId : undefined;
37
+ return {
38
+ ascApiKey: { keyP8, keyId: ascApiKeyId, issuerId: ascApiKeyIssuerId },
39
+ teamId,
40
+ };
41
+ }
42
+ }
43
+ // 2. Look up stored credentials via EAS credentials service
44
+ try {
45
+ const account = await (0, projectUtils_1.getOwnerAccountForProjectIdAsync)(graphqlClient, projectId);
46
+ const appLookupParams = {
47
+ account,
48
+ projectName: exp.slug,
49
+ bundleIdentifier: bundleId,
50
+ };
51
+ // Import dynamically to avoid circular dependency issues
52
+ const { getAscApiKeyForAppSubmissionsAsync } = await Promise.resolve().then(() => tslib_1.__importStar(require('../credentials/ios/api/GraphqlClient')));
53
+ const ascKeyFragment = await getAscApiKeyForAppSubmissionsAsync(graphqlClient, appLookupParams);
54
+ if (ascKeyFragment) {
55
+ log_1.default.log('Using App Store Connect API Key from EAS credentials service.');
56
+ const fullKey = await AppStoreConnectApiKeyQuery_1.AppStoreConnectApiKeyQuery.getByIdAsync(graphqlClient, ascKeyFragment.id);
57
+ return {
58
+ ascApiKey: {
59
+ keyP8: fullKey.keyP8,
60
+ keyId: fullKey.keyIdentifier,
61
+ issuerId: fullKey.issuerIdentifier,
62
+ },
63
+ teamId: ascKeyFragment.appleTeam?.appleTeamIdentifier,
64
+ teamName: ascKeyFragment.appleTeam?.appleTeamName ?? undefined,
65
+ };
66
+ }
67
+ }
68
+ catch (error) {
69
+ // If we can't look up credentials, that's fine — we'll fall back
70
+ log_1.default.warn(`Could not look up stored ASC API key: ${error.message}`);
71
+ }
72
+ return null;
73
+ }
19
74
  /**
20
75
  * To start syncing ASC entities, we need access to the apple utils App instance.
21
76
  * This resolves both the authentication and that App instance.
77
+ *
78
+ * Resolution order for authentication:
79
+ * 1. ASC API key from environment variables (EXPO_ASC_API_KEY_PATH, etc.)
80
+ * 2. ASC API key from submit profile (ascApiKeyPath, etc. in eas.json)
81
+ * 3. ASC API key from EAS credentials service
82
+ * 4. Interactive cookie auth (only when not in non-interactive mode)
22
83
  */
23
- async function getAppStoreAuthAsync({ projectDir, profile, exp, credentialsCtx, }) {
84
+ async function getAppStoreAuthAsync({ projectDir, profile, exp, credentialsCtx, nonInteractive, graphqlClient, projectId, }) {
24
85
  const bundleId = await resolveAppStoreBundleIdentifierAsync(projectDir, profile, exp, credentialsCtx.vcsClient);
86
+ // Try to resolve an ASC API key from profile or credentials service
87
+ const resolvedKey = await tryResolveAscApiKeyAsync({
88
+ profile,
89
+ graphqlClient,
90
+ projectId,
91
+ exp,
92
+ bundleId,
93
+ });
94
+ if (resolvedKey || (0, resolveCredentials_1.hasAscEnvVars)()) {
95
+ const authOptions = {
96
+ mode: authenticateTypes_1.AuthenticationMode.API_KEY,
97
+ ...(resolvedKey
98
+ ? {
99
+ ascApiKey: resolvedKey.ascApiKey,
100
+ teamId: resolvedKey.teamId,
101
+ teamName: resolvedKey.teamName,
102
+ // Default to COMPANY_OR_ORGANIZATION to avoid prompting for team type
103
+ teamType: authenticateTypes_1.AppleTeamType.COMPANY_OR_ORGANIZATION,
104
+ }
105
+ : {}),
106
+ };
107
+ const authCtx = await credentialsCtx.appStore.ensureAuthenticatedAsync(authOptions);
108
+ (0, assert_1.default)(authCtx.authState, 'Failed to authenticate with App Store Connect');
109
+ const app = await apple_utils_1.App.findAsync((0, authenticate_1.getRequestContext)(authCtx), { bundleId });
110
+ (0, assert_1.default)(app, `Failed to load app "${bundleId}" from App Store Connect`);
111
+ return { app, auth: authCtx.authState };
112
+ }
113
+ if (nonInteractive) {
114
+ throw new Error('No App Store Connect API Key found. In non-interactive mode, provide one via:\n' +
115
+ ' - Environment variables: EXPO_ASC_API_KEY_PATH, EXPO_ASC_KEY_ID, EXPO_ASC_ISSUER_ID\n' +
116
+ ' - eas.json submit profile: ascApiKeyPath, ascApiKeyId, ascApiKeyIssuerId\n' +
117
+ ' - EAS credentials service: run `eas credentials` to set up an API key');
118
+ }
119
+ // Fall back to interactive cookie auth
25
120
  const authCtx = await credentialsCtx.appStore.ensureAuthenticatedAsync();
26
121
  (0, assert_1.default)(authCtx.authState, 'Failed to authenticate with App Store Connect');
27
- // TODO: improve error handling by mentioning possible configuration errors
28
122
  const app = await apple_utils_1.App.findAsync((0, authenticate_1.getRequestContext)(authCtx), { bundleId });
29
123
  (0, assert_1.default)(app, `Failed to load app "${bundleId}" from App Store Connect`);
30
124
  return { app, auth: authCtx.authState };