Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This is the log of notable changes to EAS CLI and related packages.

### 🎉 New features

- Add screenshots and previews support to `metadata:push` and `metadata:pull`. ([#3301](https://github.com/expo/eas-cli/pull/3301) by [@EvanBacon](https://github.com/EvanBacon))
- Add App Clip bundle identifier registration support for multi-target iOS builds. ([#3300](https://github.com/expo/eas-cli/pull/3300) by [@evanbacon](https://github.com/evanbacon))
- Add `--runtime-version` and `--platform` filters to `eas update:list`. ([#3261](https://github.com/expo/eas-cli/pull/3261) by [@HarelSultan](https://github.com/HarelSultan))

Expand Down
2 changes: 1 addition & 1 deletion packages/eas-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "2.1.13",
"@expo/apple-utils": "2.1.14",
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "10.0.6",
"@expo/config-plugins": "9.0.12",
Expand Down
36 changes: 36 additions & 0 deletions packages/eas-cli/schema/metadata-0.json
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,42 @@
"liveEdits": true,
"optional": true
}
},
"screenshots": {
"type": "object",
"description": "Screenshots for this locale, organized by display type (e.g., APP_IPHONE_67, APP_IPAD_PRO_129)",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"previews": {
"type": "object",
"description": "Video previews for this locale, organized by display type (e.g., IPHONE_67, IPAD_PRO_129)",
"additionalProperties": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Video file path (relative to project root)"
},
"previewFrameTimeCode": {
"type": "string",
"description": "Optional preview frame time code (e.g., '00:05:00' for 5 seconds)"
}
},
"required": ["path"],
"additionalProperties": false
}
]
}
}
}
},
Expand Down
12 changes: 11 additions & 1 deletion packages/eas-cli/src/metadata/apple/config/reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import uniq from '../../../utils/expodash/uniq';
import { AttributesOf } from '../../utils/asc';
import { removeDatePrecision } from '../../utils/date';
import { AppleMetadata } from '../types';
import { AppleMetadata, ApplePreviews, AppleScreenshots } from '../types';

type PartialExcept<T, K extends keyof T> = Pick<T, K> & Partial<Omit<T, K>>;

Expand Down Expand Up @@ -211,4 +211,14 @@ export class AppleConfigReader {
// TODO: add attachment
};
}

/** Get screenshots configuration for a specific locale */
public getScreenshots(locale: string): AppleScreenshots | null {
return this.schema.info?.[locale]?.screenshots ?? null;
}

/** Get video previews configuration for a specific locale */
public getPreviews(locale: string): ApplePreviews | null {
return this.schema.info?.[locale]?.previews ?? null;
}
}
17 changes: 16 additions & 1 deletion packages/eas-cli/src/metadata/apple/config/writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '@expo/apple-utils';

import { AttributesOf } from '../../utils/asc';
import { AppleMetadata } from '../types';
import { AppleMetadata, ApplePreviews, AppleScreenshots } from '../types';

/**
* Serializes the Apple ASC entities into the metadata configuration schema.
Expand Down Expand Up @@ -186,6 +186,21 @@ export class AppleConfigWriter {
// TODO: add attachment
};
}

/** Set screenshots for a specific locale */
public setScreenshots(locale: string, screenshots: AppleScreenshots): void {
this.schema.info = this.schema.info ?? {};
this.schema.info[locale] = this.schema.info[locale] ?? { title: '' };
this.schema.info[locale].screenshots =
Object.keys(screenshots).length > 0 ? screenshots : undefined;
}

/** Set video previews for a specific locale */
public setPreviews(locale: string, previews: ApplePreviews): void {
this.schema.info = this.schema.info ?? {};
this.schema.info[locale] = this.schema.info[locale] ?? { title: '' };
this.schema.info[locale].previews = Object.keys(previews).length > 0 ? previews : undefined;
}
}

/** Helper function to convert `T | null` to `T | undefined`, required for the entity properties */
Expand Down
11 changes: 9 additions & 2 deletions packages/eas-cli/src/metadata/apple/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@
import type { AppInfoData } from './tasks/app-info';
import type { AppReviewData } from './tasks/app-review-detail';
import type { AppVersionData } from './tasks/app-version';
import type { PreviewsData } from './tasks/previews';
import type { ScreenshotsData } from './tasks/screenshots';

/**
* The fully prepared apple data, used within the `downloadAsync` or `uploadAsync` tasks.
* It contains references to each individual models, to either upload or download App Store data.
*/
export type AppleData = { app: App } & AppInfoData & AppVersionData & AgeRatingData & AppReviewData;
export type AppleData = { app: App; projectDir: string } & AppInfoData &
AppVersionData &
AgeRatingData &
AppReviewData &
ScreenshotsData &
PreviewsData;

/**
* The unprepared partial apple data, used within the `prepareAsync` tasks.
* It contains a reference to the app, each task should populate the necessary data.
* If an entity fails to prepare the data, individual tasks should raise errors about the missing data.
*/
export type PartialAppleData = { app: App } & Partial<Omit<AppleData, 'app'>>;
export type PartialAppleData = { app: App; projectDir: string } & Partial<Omit<AppleData, 'app' | 'projectDir'>>;

Check warning on line 26 in packages/eas-cli/src/metadata/apple/data.ts

View workflow job for this annotation

GitHub Actions / Test with Node 24

Replace `Omit<AppleData,·'app'·|·'projectDir'>` with `⏎··Omit<AppleData,·'app'·|·'projectDir'>⏎`
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { truthy } from '../../../utils/expodash/filter';
import { IssueRule } from '../../config/issue';
import { AppleInfo } from '../types';

const RESTRICTED_PROPERTIES: (keyof AppleInfo)[] = ['title', 'subtitle', 'description', 'keywords'];
/** Only check text properties that may contain restricted words */
type AppleInfoTextProperty = 'title' | 'subtitle' | 'description' | 'keywords';
const RESTRICTED_PROPERTIES: AppleInfoTextProperty[] = ['title', 'subtitle', 'description', 'keywords'];

Check warning on line 6 in packages/eas-cli/src/metadata/apple/rules/infoRestrictedWords.ts

View workflow job for this annotation

GitHub Actions / Test with Node 24

Replace `'title',·'subtitle',·'description',·'keywords'` with `⏎··'title',⏎··'subtitle',⏎··'description',⏎··'keywords',⏎`
const RESTRICTED_WORDS = {
beta: 'Apple restricts the word "beta" and synonyms implying incomplete functionality.',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ describe(AgeRatingTask, () => {
});

describe('uploadAsync', () => {
it('aborts when age rating is not loaded', async () => {
const promise = new AgeRatingTask().uploadAsync({
config: new AppleConfigReader({}),
context: { ageRating: undefined } as any,
});

await expect(promise).rejects.toThrow('rating not initialized');
it('skips when age rating is not loaded', async () => {
await expect(
new AgeRatingTask().uploadAsync({
config: new AppleConfigReader({}),
context: { ageRating: undefined } as any,
})
).resolves.not.toThrow();
});

it('skips updating age rating when not configured', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe(AppInfoTask, () => {

const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
projectDir: '/test/project',
};

await new AppInfoTask().prepareAsync({ context });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,20 @@ describe(AppReviewDetailTask, () => {
nock.cleanAll();
});

it('aborts when version is not loaded', async () => {
const promise = new AppReviewDetailTask().uploadAsync({
config: new AppleConfigReader({
review: {
firstName: 'Evan',
lastName: 'Bacon',
email: 'review@example.com',
phone: '+1 555 555 5555',
},
}),
context: {} as any,
});

await expect(promise).rejects.toThrow('version not init');
it('skips when version is not loaded', async () => {
await expect(
new AppReviewDetailTask().uploadAsync({
config: new AppleConfigReader({
review: {
firstName: 'Evan',
lastName: 'Bacon',
email: 'review@example.com',
phone: '+1 555 555 5555',
},
}),
context: {} as any,
})
).resolves.not.toThrow();
});

it('updates review details when loaded', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe(AppVersionTask, () => {

const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
projectDir: '/test/project',
};

await new AppVersionTask({ editLive: true }).prepareAsync({ context });
Expand Down Expand Up @@ -83,6 +84,7 @@ describe(AppVersionTask, () => {

const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
projectDir: '/test/project',
};

await new AppVersionTask({ editLive: true }).prepareAsync({ context });
Expand Down Expand Up @@ -118,6 +120,7 @@ describe(AppVersionTask, () => {

const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
projectDir: '/test/project',
};

await new AppVersionTask().prepareAsync({ context });
Expand Down Expand Up @@ -153,6 +156,7 @@ describe(AppVersionTask, () => {

const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
projectDir: '/test/project',
};

await new AppVersionTask().prepareAsync({ context });
Expand Down Expand Up @@ -194,6 +198,7 @@ describe(AppVersionTask, () => {

const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
projectDir: '/test/project',
};

await new AppVersionTask({ version: '2.0' }).prepareAsync({ context });
Expand Down Expand Up @@ -243,6 +248,7 @@ describe(AppVersionTask, () => {

const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
projectDir: '/test/project',
};

await new AppVersionTask({ version: '3.0' }).prepareAsync({ context });
Expand Down Expand Up @@ -371,6 +377,7 @@ describe(AppVersionTask, () => {

const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
projectDir: '/test/project',
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
};

Expand Down Expand Up @@ -399,6 +406,7 @@ describe(AppVersionTask, () => {

const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
projectDir: '/test/project',
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
versionPhasedRelease: null, // Not enabled yet
};
Expand All @@ -417,6 +425,7 @@ describe(AppVersionTask, () => {

const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
projectDir: '/test/project',
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
// Enabled, and not completed yet
versionPhasedRelease: new AppStoreVersionPhasedRelease(
Expand All @@ -440,8 +449,9 @@ describe(AppVersionTask, () => {

const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
projectDir: '/test/project',
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
// Enabled, and not completed yet
// Enabled, and completed
versionPhasedRelease: new AppStoreVersionPhasedRelease(
requestContext,
'APP_STORE_VERSION_PHASED_RELEASE_1',
Expand Down
11 changes: 8 additions & 3 deletions packages/eas-cli/src/metadata/apple/tasks/age-rating.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AgeRatingDeclaration } from '@expo/apple-utils';
import assert from 'assert';
import chalk from 'chalk';

import Log from '../../../log';
Expand All @@ -15,7 +14,10 @@ export class AgeRatingTask extends AppleTask {
public name = (): string => 'age rating declarations';

public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
context.ageRating = (await context.version!.getAgeRatingDeclarationAsync()) ?? undefined;
if (!context.version) {
return;
}
context.ageRating = (await context.version.getAgeRatingDeclarationAsync()) ?? undefined;
}

public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
Expand All @@ -25,7 +27,10 @@ export class AgeRatingTask extends AppleTask {
}

public async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
assert(context.ageRating, `Age rating not initialized, can't update age rating`);
if (!context.ageRating) {
Log.log(chalk`{dim - Skipped age rating update, no version available}`);
return;
}

const ageRating = config.getAgeRating();
if (!ageRating) {
Expand Down
11 changes: 8 additions & 3 deletions packages/eas-cli/src/metadata/apple/tasks/app-review-detail.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AppStoreReviewDetail } from '@expo/apple-utils';
import assert from 'assert';
import chalk from 'chalk';

import Log from '../../../log';
Expand All @@ -16,7 +15,10 @@ export class AppReviewDetailTask extends AppleTask {
public name = (): string => 'app review detail';

public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
context.reviewDetail = (await context.version!.getAppStoreReviewDetailAsync()) ?? undefined;
if (!context.version) {
return;
}
context.reviewDetail = (await context.version.getAppStoreReviewDetailAsync()) ?? undefined;
}

public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
Expand All @@ -32,7 +34,10 @@ export class AppReviewDetailTask extends AppleTask {
return;
}

assert(context.version, `App version not initialized, can't upload store review details`);
if (!context.version) {
Log.log(chalk`{dim - Skipped store review details, no version available}`);
return;
}
const { versionString } = context.version.attributes;

if (!context.reviewDetail) {
Expand Down
4 changes: 4 additions & 0 deletions packages/eas-cli/src/metadata/apple/tasks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { AgeRatingTask } from './age-rating';
import { AppInfoTask } from './app-info';
import { AppReviewDetailTask } from './app-review-detail';
import { AppVersionOptions, AppVersionTask } from './app-version';
import { PreviewsTask } from './previews';
import { ScreenshotsTask } from './screenshots';
import { AppleTask } from '../task';

type AppleTaskOptions = {
Expand All @@ -17,5 +19,7 @@ export function createAppleTasks({ version }: AppleTaskOptions = {}): AppleTask[
new AppInfoTask(),
new AgeRatingTask(),
new AppReviewDetailTask(),
new ScreenshotsTask(),
new PreviewsTask(),
];
}
Loading
Loading