Skip to content
Merged
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

- [eas-cli] 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))
- [eas-cli] Add `--non-interactive` flag to `metadata:push` and `metadata:pull` commands with ASC API Key auth support. ([#3548](https://github.com/expo/eas-cli/pull/3548) by [@EvanBacon](https://github.com/EvanBacon))

### 🐛 Bug fixes
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 @@ -43,7 +43,7 @@
"copy-new-templates": "rimraf build/commandUtils/new/templates && mkdir -p build/commandUtils/new && cp -r src/commandUtils/new/templates build/commandUtils/new"
},
"dependencies": {
"@expo/apple-utils": "2.1.15",
"@expo/apple-utils": "2.1.16",
"@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
13 changes: 11 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,25 @@ import type { AgeRatingData } from './tasks/age-rating';
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'>
>;
10 changes: 8 additions & 2 deletions packages/eas-cli/src/metadata/apple/rules/infoRestrictedWords.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
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',
];
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
@@ -1,4 +1,4 @@
import { AgeRatingDeclaration, AppStoreVersion, Rating } from '@expo/apple-utils';
import { AgeRatingDeclaration, AppInfo, Rating } from '@expo/apple-utils';
import nock from 'nock';

import { requestContext } from './fixtures/requestContext';
Expand All @@ -12,20 +12,30 @@ jest.mock('../../config/writer');

describe(AgeRatingTask, () => {
describe('prepareAsync', () => {
it('loads age rating from app version', async () => {
it('loads age rating from app info', async () => {
const scope = nock('https://api.appstoreconnect.apple.com')
.get(`/v1/${AppStoreVersion.type}/stub-id/ageRatingDeclaration`)
.get(`/v1/${AppInfo.type}/stub-id/ageRatingDeclaration`)
.reply(200, require('./fixtures/appStoreVersions/get-ageRatingDeclaration-200.json'));

const context: any = {
version: new AppStoreVersion(requestContext, 'stub-id', {} as any),
info: new AppInfo(requestContext, 'stub-id', {} as any),
};

await new AgeRatingTask().prepareAsync({ context });

expect(context.ageRating).toBeInstanceOf(AgeRatingDeclaration);
expect(scope.isDone()).toBeTruthy();
});

it('skips when info is not available', async () => {
const context: any = {
info: undefined,
};

await new AgeRatingTask().prepareAsync({ context });

expect(context.ageRating).toBeUndefined();
});
});

describe('downloadAsync', () => {
Expand Down Expand Up @@ -54,13 +64,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
Loading
Loading