Compare commits

...
Author SHA1 Message Date
Cedric van Putten dbdf8a0ab1 [eas-cli] Add metadata config command (#1117)
* [eas-cli] Add config writer to serialize back to schema

* [eas-cli] Add config writer to pull data from ASC

* [eas-cli] Fix wrong entity used for categories

* [eas-cli] Clean up prepare tests without reader property

* [eas-cli] Add missing ora mocks

* [eas-cli] Move metadata to its own command

* [eas-cli] Finalize the initial draft of metadata
2022-05-31 04:45:58 -07:00
Cedric van Putten 4de958d636 [eas-cli] Add metadata telemetry and error reporting (#1116)
* [cli] Upgrade apple-utils with axios access

* [eas-cli] Fix schema not found by moving it outside source

* [eas-cli] Add metadata telemetry and error reporting

* [eas-cli] Add tests for telemetry data scrubber

* [eas-cli] Add metadata error documentation
2022-05-30 06:03:48 -07:00
Cedric van Putten 5c57739511 [eas-cli] Fix linting issue 2022-05-23 15:42:18 +02:00
Cedric van Putten 0c38b98f63 [eas-cli] Add workaround for axios and nock tests 2022-05-23 15:41:42 +02:00
Cedric van Putten 0719abbd38 [metadata] Add first draft of app store connect metadata sync 2022-05-23 15:31:00 +02:00
Dominik Sokal f0c958e58b Drop support for node <14 (#1098)
* drop support for node 12 and 13

* update volta pinned node and yarn versions

* update node version in CI

* upgrade memfs to latest
2022-05-19 10:21:09 +02:00
Dominik Sokal 607838c8e1 [ENG-4968] better spinner placement for multiple builds (#1105)
* better spinner placement for multiple builds

* update CHANGELOG.md
2022-05-17 09:10:00 +02:00
Dominik Sokal 34ecd38304 Update dependencies (#1095)
* update outdated deps

* [eas-cli] regenerate GraphQL code

* [eas-cli] fix tests

* [scripts] upgrade deps

* [eas-cli] fix lint warnings

* update CHANGELOG.md
2022-05-11 11:20:22 +02:00
Will Schurman c4adf7056e [eas-cli] Replace asMock with jest.mocked (#1089) 2022-05-02 12:56:16 -07:00
Expo CI 0e7f326de2 update CHANGELOG.md 2022-04-26 13:10:13 +00:00
Wojciech Kozyra bbc8b1ac76 v0.52.0 2022-04-26 14:49:58 +02:00
Wojciech Kozyra 3db2a69183 upgrade eas-cli-local-build-plugin to 0.0.78 2022-04-26 14:49:48 +02:00
Wojciech Kozyra 51155bb2b4 [eas-cli] update eas-build-job (#1084) 2022-04-26 14:11:52 +02:00
Wojciech Kozyra 30ec5f967b [eas-cli] Support target specific entitlements (#1078)
* [eas-cli] Support target specific entitlements

* reiview feedback

* fix tests

* review feedback

* update CHANGELOG.md
2022-04-26 14:11:30 +02:00
159 changed files with 6434 additions and 2131 deletions

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: 18
- name: Checkout Pull Request
run: hub pr checkout ${{ github.event.issue.number }}
env:
+5 -5
View File
@@ -27,7 +27,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: 18
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Build tarballs
@@ -68,7 +68,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: 18
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Build tarballs
@@ -96,7 +96,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: 18
- run: yarn install --frozen-lockfile --check-files
- run: sudo apt-get install nsis
- run: yarn build
@@ -138,7 +138,7 @@ jobs:
with:
registry-url: 'https://registry.npmjs.org/'
scope: 'expo'
node-version: '16'
node-version: 18
- name: Install dependencies
run: yarn install --frozen-lockfile --check-files
- name: Build
@@ -155,7 +155,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: 18
- name: Install dependencies
working-directory: ./scripts
run: yarn install --frozen-lockfile --check-files
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
node-version: 18
- uses: preactjs/compressed-size-action@v2
with:
build-script: 'build:tarballs:linux'
+2 -2
View File
@@ -10,9 +10,9 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: ['12', '14', '16']
node: ['14', '16', '18']
include:
- node: '16'
- node: '18'
coverage: true
name: Test with Node ${{ matrix.node }}
steps:
+9
View File
@@ -12,6 +12,15 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores
- Update dependencies. ([#1095](https://github.com/expo/eas-cli/pull/1095) by [@dsokal](https://github.com/dsokal))
- Better spinner placement for multiple builds. ([#1105](https://github.com/expo/eas-cli/pull/1105) by [@dsokal](https://github.com/dsokal))
## [0.52.0](https://github.com/expo/eas-cli/releases/tag/v0.52.0) - 2022-04-26
### 🎉 New features
- Support target specific entitlements. ([#1078](https://github.com/expo/eas-cli/pull/1078) by [@wkozyra95](https://github.com/wkozyra95))
## [0.51.0](https://github.com/expo/eas-cli/releases/tag/v0.51.0) - 2022-04-19
### 🎉 New features
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.51.0"
"version": "0.52.0"
}
+12 -12
View File
@@ -17,22 +17,22 @@
"clean": "lerna run clean && rm -rf node_modules coverage"
},
"devDependencies": {
"@types/jest": "27.4.1",
"@types/node": "16.7.8",
"eslint": "8.12.0",
"eslint-config-universe": "10.0.0",
"@types/jest": "27.5.0",
"@types/node": "17.0.32",
"eslint": "8.15.0",
"eslint-config-universe": "11.0.0",
"eslint-plugin-async-protect": "1.1.0",
"eslint-plugin-node": "11.1.0",
"jest": "27.5.1",
"jest-watch-typeahead": "1.0.0",
"jest": "28.1.0",
"jest-watch-typeahead": "1.1.0",
"lerna": "4.0.0",
"oclif": "2.6.3",
"prettier": "2.6.1",
"ts-jest": "27.1.4",
"typescript": "4.6.3"
"oclif": "3.0.1",
"prettier": "2.6.2",
"ts-jest": "28.0.2",
"typescript": "4.6.4"
},
"volta": {
"node": "16.10.0",
"yarn": "1.22.15"
"node": "18.2.0",
"yarn": "1.22.18"
}
}
+42 -42
View File
@@ -98,7 +98,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -115,7 +115,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -132,7 +132,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -146,7 +146,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -195,7 +195,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -215,7 +215,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -232,7 +232,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -251,7 +251,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -271,7 +271,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -300,7 +300,7 @@ DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -314,7 +314,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -331,7 +331,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
@@ -366,7 +366,7 @@ DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -399,7 +399,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:submit`
@@ -444,7 +444,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -464,7 +464,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -485,7 +485,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -502,7 +502,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:view [NAME]`
@@ -522,7 +522,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -540,7 +540,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -554,7 +554,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -568,7 +568,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -586,7 +586,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -603,7 +603,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -617,7 +617,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -631,7 +631,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -710,7 +710,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -727,7 +727,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
@@ -747,7 +747,7 @@ DESCRIPTION
create an environment secret on the current project or owner account
```
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -764,7 +764,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -778,7 +778,7 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/secret/list.ts)_
## `eas submit`
@@ -808,7 +808,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -838,7 +838,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/update/index.ts)_
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -855,7 +855,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -875,7 +875,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
@@ -894,7 +894,7 @@ DESCRIPTION
view the recent updates for a branch
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/update/list.ts)_
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:view GROUPID`
@@ -914,7 +914,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/update/view.ts)_
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -934,7 +934,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -951,7 +951,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -968,7 +968,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -989,7 +989,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1006,7 +1006,7 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.51.0/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.52.0/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
+559 -90
View File
@@ -701,7 +701,7 @@
},
{
"name": "billing",
"description": "Billing information",
"description": "Billing information. Only visible to members with the ADMIN or OWNER role.",
"args": [],
"type": {
"kind": "OBJECT",
@@ -1702,7 +1702,7 @@
"deprecationReason": null
},
{
"name": "chargeIdentifier",
"name": "chargeID",
"description": null,
"type": {
"kind": "NON_NULL",
@@ -1716,6 +1716,30 @@
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "description",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "reason",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
@@ -2088,15 +2112,15 @@
"description": null,
"fields": [
{
"name": "metric",
"name": "id",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EASServiceMetric",
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
@@ -2119,6 +2143,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "serviceMetric",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EASServiceMetric",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "timestamp",
"description": null,
@@ -2157,11 +2197,151 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AccountUsageMetricAndCost",
"description": null,
"fields": [
{
"name": "id",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "limit",
"description": "The limit, in units, allowed by this plan",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Float",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "metricType",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "UsageMetricType",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "serviceMetric",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EASServiceMetric",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "totalCost",
"description": "Total cost of this particular metric, in cents",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Float",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "value",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Float",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AccountUsageMetrics",
"description": null,
"fields": [
{
"name": "byBillingPeriod",
"description": null,
"args": [
{
"name": "date",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "UsageMetricTotal",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "metricsForServiceMetric",
"description": null,
@@ -2183,7 +2363,7 @@
"deprecationReason": null
},
{
"name": "metric",
"name": "serviceMetric",
"description": null,
"type": {
"kind": "NON_NULL",
@@ -6485,15 +6665,15 @@
"fields": [
{
"name": "colorPalette",
"description": "Nullable color palette of the app icon. If null, color palette couldn't be retrieved from external service (imgix)",
"description": null,
"args": [],
"type": {
"kind": "SCALAR",
"name": "JSON",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
"isDeprecated": true,
"deprecationReason": "No longer supported"
},
{
"name": "originalUrl",
@@ -6545,6 +6725,29 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AppInfoInput",
"description": null,
"fields": null,
"inputFields": [
{
"name": "displayName",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AppInput",
@@ -6567,6 +6770,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appInfo",
"description": null,
"type": {
"kind": "INPUT_OBJECT",
"name": "AppInfoInput",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "privacy",
"description": null,
@@ -6683,6 +6898,55 @@
"isDeprecated": true,
"deprecationReason": "No longer supported"
},
{
"name": "setAppInfo",
"description": "Set display info for app",
"args": [
{
"name": "appId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appInfo",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "AppInfoInput",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "App",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "setPushSecurityEnabled",
"description": "Require api token to send push notifs for experience",
@@ -13022,6 +13286,65 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Concurrencies",
"description": null,
"fields": [
{
"name": "android",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ios",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "total",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "CreateAccessTokenInput",
@@ -13126,6 +13449,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveUrl",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "config",
"description": null,
@@ -13263,6 +13598,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveUrl",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "config",
"description": null,
@@ -13296,77 +13643,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "CreateSubmissionInput",
"description": null,
"fields": null,
"inputFields": [
{
"name": "appId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "config",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "JSONObject",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "platform",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "AppPlatform",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "submittedBuildId",
"description": null,
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "CreateSubmissionResult",
@@ -13992,11 +14268,29 @@
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "ASSETS_REQUESTS",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "BANDWIDTH_USAGE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "MANIFEST_REQUESTS",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "UNIQUE_USERS",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
@@ -21089,6 +21383,34 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveUrl",
"description": null,
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "canRetry",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "cancelingActor",
"description": null,
@@ -21181,6 +21503,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "parentSubmission",
"description": null,
"args": [],
"type": {
"kind": "OBJECT",
"name": "Submission",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "platform",
"description": null,
@@ -21429,9 +21763,13 @@
"name": "submissionId",
"description": null,
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
@@ -21517,18 +21855,18 @@
"deprecationReason": null
},
{
"name": "createSubmission",
"description": "Create an EAS Submit submission",
"name": "retrySubmission",
"description": "Retry an EAS Submit submission",
"args": [
{
"name": "input",
"name": "parentSubmissionId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "CreateSubmissionInput",
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
@@ -21546,8 +21884,8 @@
"ofType": null
}
},
"isDeprecated": true,
"deprecationReason": "Use createIosSubmission / createAndroidSubmission instead"
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -21687,6 +22025,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "concurrencies",
"description": null,
"args": [],
"type": {
"kind": "OBJECT",
"name": "Concurrencies",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "endedAt",
"description": null,
@@ -22979,6 +23329,113 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "UsageMetricTotal",
"description": null,
"fields": [
{
"name": "billingPeriod",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "BillingPeriod",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "overageMetrics",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "AccountUsageMetricAndCost",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "planMetrics",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "AccountUsageMetricAndCost",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "totalCost",
"description": "Total cost of overages, in cents",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Float",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "UsageMetricType",
@@ -22987,11 +23444,23 @@
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "BANDWIDTH",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "REQUEST",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "USER",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
+34 -33
View File
@@ -1,38 +1,39 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.51.0",
"version": "0.52.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.31",
"@expo/apple-utils": "0.0.0-alpha.32",
"@expo/code-signing-certificates": "0.0.2",
"@expo/config": "6.0.19",
"@expo/config-plugins": "4.1.0",
"@expo/config-types": "44.0.0",
"@expo/eas-build-job": "0.2.68",
"@expo/eas-json": "0.51.0",
"@expo/json-file": "8.2.34",
"@expo/config": "6.0.23",
"@expo/config-plugins": "4.1.4",
"@expo/config-types": "45.0.0",
"@expo/eas-build-job": "0.2.72",
"@expo/eas-json": "0.52.0",
"@expo/json-file": "8.2.36",
"@expo/multipart-body-parser": "1.1.0",
"@expo/pkcs12": "0.0.7",
"@expo/plist": "0.0.17",
"@expo/pkcs12": "0.0.8",
"@expo/plist": "0.0.18",
"@expo/plugin-warn-if-update-available": "2.1.0",
"@expo/prebuild-config": "3.1.0",
"@expo/prebuild-config": "3.1.6",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.6.0",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "1.6.4",
"@oclif/plugin-autocomplete": "1.2.0",
"@oclif/core": "1.8.0",
"@oclif/plugin-autocomplete": "1.3.0",
"@oclif/plugin-help": "5.1.12",
"@urql/core": "2.4.3",
"@urql/exchange-retry": "0.3.2",
"@urql/core": "2.4.4",
"@urql/exchange-retry": "0.3.3",
"ajv": "^6.12.6",
"chalk": "4.1.2",
"cli-progress": "3.10.0",
"cli-table3": "0.6.1",
"cli-progress": "3.11.0",
"cli-table3": "0.6.2",
"dateformat": "4.6.3",
"env-paths": "2.2.0",
"envinfo": "7.8.1",
@@ -40,12 +41,12 @@
"fast-glob": "3.2.11",
"figures": "3.2.0",
"form-data": "4.0.0",
"fs-extra": "10.0.1",
"fs-extra": "10.1.0",
"getenv": "1.0.0",
"gradle-to-js": "2.0.0",
"graphql": "16.3.0",
"gradle-to-js": "2.0.1",
"graphql": "16.5.0",
"graphql-tag": "2.12.6",
"https-proxy-agent": "5.0.0",
"https-proxy-agent": "5.0.1",
"ignore": "5.2.0",
"indent-string": "4.0.0",
"jks-js": "1.0.2",
@@ -53,8 +54,8 @@
"keychain": "1.3.0",
"log-symbols": "4.1.0",
"mime": "3.0.0",
"minimatch": "3.0.5",
"nanoid": "3.3.2",
"minimatch": "5.0.1",
"nanoid": "3.3.4",
"node-fetch": "2.6.7",
"node-forge": "1.3.1",
"nullthrows": "1.1.1",
@@ -63,12 +64,12 @@
"prompts": "2.4.2",
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.3.5",
"semver": "7.3.7",
"slash": "3.0.0",
"strip-ansi": "6.0.0",
"tar": "6.1.11",
"terminal-link": "2.1.1",
"tslib": "2.3.1",
"tslib": "2.4.0",
"turndown": "7.1.1",
"untildify": "4.0.0",
"uuid": "8.3.2",
@@ -77,8 +78,8 @@
"devDependencies": {
"@graphql-codegen/cli": "2.6.2",
"@graphql-codegen/introspection": "2.1.1",
"@graphql-codegen/typescript": "2.4.8",
"@graphql-codegen/typescript-operations": "2.3.5",
"@graphql-codegen/typescript": "2.4.11",
"@graphql-codegen/typescript-operations": "2.4.0",
"@types/cli-progress": "3.9.2",
"@types/dateformat": "3.0.1",
"@types/envinfo": "7.8.1",
@@ -86,25 +87,25 @@
"@types/getenv": "^1.0.0",
"@types/mime": "2.0.3",
"@types/node-fetch": "^2.5.7",
"@types/node-forge": "1.0.1",
"@types/node-forge": "1.0.2",
"@types/prompts": "2.0.14",
"@types/semver": "7.3.9",
"@types/tar": "6.1.1",
"@types/tough-cookie": "4.0.1",
"@types/tough-cookie": "4.0.2",
"@types/uuid": "8.3.4",
"@types/wrap-ansi": "3.0.0",
"axios": "0.26.1",
"axios": "0.27.2",
"eslint-plugin-graphql": "4.0.0",
"form-data": "4.0.0",
"memfs": "3.4.1",
"memfs": "3.4.3",
"mockdate": "3.0.5",
"nock": "13.2.4",
"tempy": "0.7.0",
"ts-deepmerge": "2.0.1",
"typescript": "4.6.3"
"typescript": "4.6.4"
},
"engines": {
"node": ">=12.0.0"
"node": ">=14.0.0"
},
"files": [
"/bin",
+928
View File
@@ -0,0 +1,928 @@
{
"$schema": "http://json-schema.org/draft-06/schema",
"type": "object",
"definitions": {
"apple": {
"AppleLocalizedPreviewFolder": {
"anyOf": [
{
"description": "Path to folder containing preview files",
"type": "string"
},
{
"type": "array",
"uniqueItems": true,
"items": {
"type": "string"
}
}
]
},
"AppleKidsAge": {
"enum": ["FIVE_AND_UNDER", "SIX_TO_EIGHT", "NINE_TO_ELEVEN"]
},
"AppleRating": {
"enum": ["NONE", "INFREQUENT_OR_MILD", "FREQUENT_OR_INTENSE"]
},
"AppleCategory": {
"enum": [
"FOOD_AND_DRINK",
"BUSINESS",
"EDUCATION",
"SOCIAL_NETWORKING",
"BOOKS",
"SPORTS",
"FINANCE",
"REFERENCE",
"GRAPHICS_AND_DESIGN",
"DEVELOPER_TOOLS",
"HEALTH_AND_FITNESS",
"MUSIC",
"WEATHER",
"TRAVEL",
"ENTERTAINMENT",
"STICKERS",
"GAMES",
"LIFESTYLE",
"MEDICAL",
"MAGAZINES_AND_NEWSPAPERS",
"UTILITIES",
"SHOPPING",
"PRODUCTIVITY",
"NEWS",
"PHOTO_AND_VIDEO",
"NAVIGATION"
]
},
"AppleSubcategory": {
"enum": [
"STICKERS_PLACES_AND_OBJECTS",
"STICKERS_EMOJI_AND_EXPRESSIONS",
"STICKERS_CELEBRATIONS",
"STICKERS_CELEBRITIES",
"STICKERS_MOVIES_AND_TV",
"STICKERS_SPORTS_AND_ACTIVITIES",
"STICKERS_EATING_AND_DRINKING",
"STICKERS_CHARACTERS",
"STICKERS_ANIMALS",
"STICKERS_FASHION",
"STICKERS_ART",
"STICKERS_GAMING",
"STICKERS_KIDS_AND_FAMILY",
"STICKERS_PEOPLE",
"STICKERS_MUSIC",
"GAMES_SPORTS",
"GAMES_WORD",
"GAMES_MUSIC",
"GAMES_ADVENTURE",
"GAMES_ACTION",
"GAMES_ROLE_PLAYING",
"GAMES_CASUAL",
"GAMES_BOARD",
"GAMES_TRIVIA",
"GAMES_CARD",
"GAMES_PUZZLE",
"GAMES_CASINO",
"GAMES_STRATEGY",
"GAMES_SIMULATION",
"GAMES_RACING",
"GAMES_FAMILY"
]
},
"AppleIdfa": {
"description": "Identifier for Advertisers",
"type": "object",
"additionalProperties": false,
"defaultSnippets": [
{
"body": {
"servesAds": false,
"attributesAppInstallationToPreviousAd": false,
"attributesActionWithPreviousAd": false,
"honorsLimitedAdTracking": false
}
}
],
"properties": {
"servesAds": {
"type": "boolean"
},
"attributesAppInstallationToPreviousAd": {
"type": "boolean"
},
"attributesActionWithPreviousAd": {
"type": "boolean"
},
"honorsLimitedAdTracking": {
"type": "boolean"
}
}
},
"AppleInfo": {
"type": "object",
"additionalProperties": false,
"required": [
"title",
"subtitle",
"description",
"keywords",
"marketingUrl",
"supportUrl",
"privacyPolicyUrl"
],
"defaultSnippets": [
{
"label": "Basic",
"body": {
}
},
{
"label": "Interactive",
"description": "Creates localized app info",
"body": {
"title": "$1",
"subtitle": "$2",
"description": "$3",
"keywords": [],
"marketingUrl": "",
"supportUrl": "",
"privacyPolicyUrl": ""
}
}
],
"properties": {
"title": {
"description": "Name of the app in the store. This should be similar to the installed app name.",
"type": "string",
"maxLength": 30,
"meta": {
"storeInfo": "The name will be reviewed before it is made available on the App Store."
}
},
"subtitle": {
"description": "Subtext for the app. Example: 'A Fun Game For Friends'",
"type": "string",
"maxLength": 30,
"meta": {
"storeInfo": "The subtitle will be reviewed before it is made available on the App Store."
}
},
"description": {
"meta": {
"versioned": true,
"liveEdits": true
},
"maxLength": 4000,
"description": "Main description of what the app does",
"type": "string"
},
"promoText": {
"meta": {
"versioned": true,
"liveEdits": true,
"aso": false
},
"maxLength": 170,
"description": "Short tagline for the app",
"type": "string"
},
"keywords": {
"meta": {
"versioned": true
},
"description": "List of keywords to help users find the app in the App Store",
"type": "array",
"uniqueItems": true,
"items": {
"maxLength": 100,
"type": "string"
}
},
"privacyPolicyText": {
"type": "string",
"description": "Privacy policy for Apple TV"
},
"whatsNew": {
"meta": {
"versioned": true,
"liveEdits": true
},
"maxLength": 4000,
"description": "Changes since the last public version",
"type": "string"
},
"marketingUrl": {
"meta": {
"versioned": true,
"liveEdits": true
},
"description": "URL to the app marketing page",
"type": "string"
},
"supportUrl": {
"meta": {
"versioned": true,
"liveEdits": true
},
"description": "URL to the app support page",
"type": "string"
},
"privacyPolicyUrl": {
"meta": {
"versioned": true,
"liveEdits": true
},
"maxLength": 255,
"description": "A URL that links to your privacy policy. A privacy policy is required for all apps.",
"type": "string"
},
"privacyChoiceUrl": {
"meta": {
"versioned": true,
"liveEdits": true,
"optional": true
},
"maxLength": 255,
"description": "A URL where users can modify and delete the data collected from the app, or decide how their data is used and shared.",
"type": "string"
}
}
},
"AppleAdvisory": {
"type": "object",
"additionalProperties": false,
"defaultSnippets": [
{
"label": "Minimum",
"description": "The bare minimum rating, your app does nothing",
"body": {
"alcoholTobaccoOrDrugUseOrReferences": "NONE",
"contests": "NONE",
"gamblingSimulated": "NONE",
"medicalOrTreatmentInformation": "NONE",
"profanityOrCrudeHumor": "NONE",
"sexualContentGraphicAndNudity": "NONE",
"sexualContentOrNudity": "NONE",
"horrorOrFearThemes": "NONE",
"matureOrSuggestiveThemes": "NONE",
"violenceCartoonOrFantasy": "NONE",
"violenceRealisticProlongedGraphicOrSadistic": "NONE",
"violenceRealistic": "NONE",
"gamblingAndContests":false,
"gambling": false,
"kidsAgeBand": null,
"seventeenPlus": false,
"unrestrictedWebAccess": false
}
},
{
"label": "Interactive",
"description": "Step through options",
"body": {
"alcoholTobaccoOrDrugUseOrReferences": "${1|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"contests": "${2|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"gamblingSimulated": "${3|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"medicalOrTreatmentInformation": "${4|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"profanityOrCrudeHumor": "${5|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"sexualContentGraphicAndNudity": "${6|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"sexualContentOrNudity": "${7|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"horrorOrFearThemes": "${8|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"matureOrSuggestiveThemes": "${9|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"violenceCartoonOrFantasy": "${10|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"violenceRealisticProlongedGraphicOrSadistic": "${11|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"violenceRealistic": "${12|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
"gamblingAndContests":false,
"gambling": false,
"kidsAgeBand": null,
"seventeenPlus": false,
"unrestrictedWebAccess": false
}
}
],
"properties": {
"alcoholTobaccoOrDrugUseOrReferences": {
"$ref": "#/definitions/apple/AppleRating"
},
"contests": {
"$ref": "#/definitions/apple/AppleRating"
},
"gamblingAndContests": {
"type": "boolean"
},
"gambling": {
"type": "boolean"
},
"gamblingSimulated": {
"$ref": "#/definitions/apple/AppleRating"
},
"kidsAgeBand": {
"anyOf": [
{
"$ref": "#/definitions/apple/AppleKidsAge"
},
{
"type": "null"
}
]
},
"medicalOrTreatmentInformation": {
"$ref": "#/definitions/apple/AppleRating"
},
"profanityOrCrudeHumor": {
"$ref": "#/definitions/apple/AppleRating"
},
"sexualContentGraphicAndNudity": {
"$ref": "#/definitions/apple/AppleRating"
},
"sexualContentOrNudity": {
"$ref": "#/definitions/apple/AppleRating"
},
"seventeenPlus": {
"type": "boolean"
},
"horrorOrFearThemes": {
"$ref": "#/definitions/apple/AppleRating"
},
"matureOrSuggestiveThemes": {
"$ref": "#/definitions/apple/AppleRating"
},
"unrestrictedWebAccess": {
"type": "boolean"
},
"violenceCartoonOrFantasy": {
"$ref": "#/definitions/apple/AppleRating"
},
"violenceRealisticProlongedGraphicOrSadistic": {
"$ref": "#/definitions/apple/AppleRating"
},
"violenceRealistic": {
"$ref": "#/definitions/apple/AppleRating"
}
}
},
"AppleRelease": {
"type": "object",
"additionalProperties": false,
"defaultSnippets": [
{
"body": {
"automaticRelease": true,
"usesThirdPartyContent": false,
"usesNonExemptEncryption": false
}
}
],
"properties": {
"autoReleaseDate": {
"description": "ISO Date to release the app automatically. Leave undefined to manually release the app to the public.",
"defaultSnippets": [
{
"body": "$CURRENT_YEAR-${1:$CURRENT_MONTH}-${2:$CURRENT_DATE}"
}
],
"anyOf": [
{
"type": "string",
"format": "date-time"
},
{
"type": "string",
"format": "date"
},
{
"type": "number"
}
]
},
"isPhasedReleaseEnabled": {
"type": "boolean",
"description": "Release update over 7-day period instead of instantly.",
"meta": {
"storeInfo": "Phased release for automatic updates lets you gradually release this update over a 7-day period to users who have turned on automatic updates. Keep in mind that this version will still be available to all users as a manual update from the App Store. You can pause the phased release for up to 30 days or release this update to all users at any time. [Learn More](https://help.apple.com/app-store-connect/#/dev3d65fcee1)."
}
},
"shouldResetRatings": {
"description": "Should App Store ratings be reset for this version of the app.",
"type": "boolean",
"meta": {
"storeInfo": "You can reset your app’s summary rating for all countries or regions when you release this version. Keep in mind that once this version is released, you won’t be able to restore the rating. Your app’s existing customer reviews will still appear on the App Store."
}
},
"usesThirdPartyContent": {
"type": "boolean"
},
"automaticRelease": {
"description": "Should the app automatically be released when approved by App Store review.",
"type": "boolean"
},
"usesNonExemptEncryption": {
"description": "Alternative to setting `ITSAppUsesNonExemptEncryption` in the binary's `Info.plist`.",
"type": "boolean"
}
}
},
"AppleReview": {
"type": "object",
"description": "Info provided to the App Store review team to help them understand how to use your app.",
"required": ["firstName", "lastName"],
"defaultSnippets": [
{
"body": {
"firstName": "$1",
"lastName": "$2"
}
}
],
"properties": {
"demoUsername": {
"description": "Optional username for testing the app.",
"type": "string"
},
"demoPassword": {
"description": "Optional password for testing the app.",
"type": "string"
},
"attachment": {
"description": "Local file path to upload to the review team.",
"type": "string"
},
"firstName": {
"description": "Developer first name for the reviewer to contact.",
"type": "string"
},
"lastName": {
"description": "Developer last name (surname) for the reviewer to contact.",
"type": "string"
},
"phone": {
"description": "Developer phone number for the reviewer to contact.",
"type": "string"
},
"email": {
"description": "Developer email address for the reviewer to contact.",
"type": "string",
"format": "email"
},
"notes": {
"description": "Any additional notes for the App Store reviewer.",
"type": "string"
}
}
}
},
"AppleConfig": {
"description": "Configuration that is specific to the Apple App Store.",
"type": "object",
"additionalProperties": false,
"properties": {
"advisory": {
"$ref": "#/definitions/apple/AppleAdvisory"
},
"categories": {
"type": "array",
"maxItems": 2,
"description": "App Store categories for the app. Can add up to two categories. STICKERS and GAMES categories can add an additional subcategory.",
"items": {
"anyOf": [
{
"$ref": "#/definitions/apple/AppleCategory"
},
{
"type": "array",
"maxItems": 2,
"items": [
{
"enum": ["GAMES"]
},
{
"enum": [
"GAMES_SPORTS",
"GAMES_WORD",
"GAMES_MUSIC",
"GAMES_ADVENTURE",
"GAMES_ACTION",
"GAMES_ROLE_PLAYING",
"GAMES_CASUAL",
"GAMES_BOARD",
"GAMES_TRIVIA",
"GAMES_CARD",
"GAMES_PUZZLE",
"GAMES_CASINO",
"GAMES_STRATEGY",
"GAMES_SIMULATION",
"GAMES_RACING",
"GAMES_FAMILY"
]
}
],
"additionalItems": false
},
{
"type": "array",
"maxItems": 2,
"items": [
{
"enum": ["STICKERS"]
},
{
"enum": [
"STICKERS_PLACES_AND_OBJECTS",
"STICKERS_EMOJI_AND_EXPRESSIONS",
"STICKERS_CELEBRATIONS",
"STICKERS_CELEBRITIES",
"STICKERS_MOVIES_AND_TV",
"STICKERS_SPORTS_AND_ACTIVITIES",
"STICKERS_EATING_AND_DRINKING",
"STICKERS_CHARACTERS",
"STICKERS_ANIMALS",
"STICKERS_FASHION",
"STICKERS_ART",
"STICKERS_GAMING",
"STICKERS_KIDS_AND_FAMILY",
"STICKERS_PEOPLE",
"STICKERS_MUSIC"
]
}
],
"additionalItems": false
}
]
}
},
"copyright": {
"defaultSnippets": [
{
"body": "$CURRENT_YEAR ${1:name}"
}
],
"meta": {
"versioned": true,
"liveEdits": true,
"storeInfo": "The name of the person or entity that owns the exclusive rights to your app, preceded by the year the rights were obtained (for example, \"2008 Acme Inc.\"). Do not provide a URL."
},
"description": "The updated company copyright. Example: 2021 Evan Bacon",
"type": "string"
},
"idfa": {
"$ref": "#/definitions/apple/AppleIdfa"
},
"info": {
"description": "Localized core app info",
"type": "object",
"additionalProperties": false,
"properties": {
"ar-SA": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Arabic"
},
"ca": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Catalan"
},
"zh-Hans": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Chinese (Simplified)"
},
"zh-Hant": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Chinese (Traditional)"
},
"hr": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Croatian"
},
"cs": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Czech"
},
"da": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Danish"
},
"nl-NL": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Dutch"
},
"en-AU": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "English (Australia)"
},
"en-CA": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "English (Canada)"
},
"en-GB": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "English (U.K.)"
},
"en-US": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "English (U.S.)"
},
"fi": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Finnish"
},
"fr-FR": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "French"
},
"fr-CA": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "French (Canada)"
},
"de-DE": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "German"
},
"el": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Greek"
},
"he": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Hebrew"
},
"hi": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Hindi"
},
"hu": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Hungarian"
},
"id": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Indonesian"
},
"it": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Italian"
},
"ja": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Japanese"
},
"ko": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Korean"
},
"ms": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Malay"
},
"no": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Norwegian"
},
"pl": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Polish"
},
"pt-BR": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Portuguese (Brazil)"
},
"pt-PT": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Portuguese (Portugal)"
},
"ro": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Romanian"
},
"ru": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Russian"
},
"sk": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Slovak"
},
"es-MX": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Spanish (Mexico)"
},
"es-ES": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Spanish (Spain)"
},
"sv": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Swedish"
},
"th": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Thai"
},
"tr": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Turkish"
},
"uk": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Ukrainian"
},
"vi": {
"$ref": "#/definitions/apple/AppleInfo",
"description": "Vietnamese"
}
}
},
"preview": {
"anyOf": [
{
"description": "Path to localized folders containing preview files",
"type": "string"
},
{
"description": "Localized previews object. Ex: `\"en-US\": [\"image.png\"]`",
"type": "object",
"additionalProperties": false,
"properties": {
"ar-SA": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Arabic"
},
"ca": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Catalan"
},
"zh-Hans": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Chinese (Simplified)"
},
"zh-Hant": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Chinese (Traditional)"
},
"hr": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Croatian"
},
"cs": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Czech"
},
"da": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Danish"
},
"nl-NL": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Dutch"
},
"en-AU": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "English (Australia)"
},
"en-CA": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "English (Canada)"
},
"en-GB": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "English (U.K.)"
},
"en-US": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "English (U.S.)"
},
"fi": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Finnish"
},
"fr-FR": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "French"
},
"fr-CA": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "French (Canada)"
},
"de-DE": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "German"
},
"el": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Greek"
},
"he": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Hebrew"
},
"hi": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Hindi"
},
"hu": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Hungarian"
},
"id": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Indonesian"
},
"it": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Italian"
},
"ja": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Japanese"
},
"ko": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Korean"
},
"ms": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Malay"
},
"no": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Norwegian"
},
"pl": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Polish"
},
"pt-BR": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Portuguese (Brazil)"
},
"pt-PT": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Portuguese (Portugal)"
},
"ro": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Romanian"
},
"ru": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Russian"
},
"sk": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Slovak"
},
"es-MX": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Spanish (Mexico)"
},
"es-ES": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Spanish (Spain)"
},
"sv": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Swedish"
},
"th": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Thai"
},
"tr": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Turkish"
},
"uk": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Ukrainian"
},
"vi": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Vietnamese"
}
}
}
]
},
"release": {
"description": "Release strategy",
"$ref": "#/definitions/apple/AppleRelease"
},
"review": {
"description": "Info for the App Store reviewer",
"$ref": "#/definitions/apple/AppleReview"
},
"priceTier": {
"description": "App price tier. 0 is Free.",
"type": "number"
}
}
}
},
"properties": {
"configVersion": {
"const": 0
},
"apple": {
"$ref": "#/definitions/AppleConfig"
}
},
"additionalProperties": false,
"required": ["configVersion", "apple"]
}
-3
View File
@@ -1,3 +0,0 @@
export function asMock(fn: any): jest.Mock {
return fn as jest.Mock;
}
+6 -1
View File
@@ -1,5 +1,5 @@
import { logEvent } from './rudderstackClient';
export type Event = BuildEvent | SubmissionEvent;
export type Event = BuildEvent | SubmissionEvent | MetadataEvent;
export enum SubmissionEvent {
SUBMIT_COMMAND = 'submit cli submit command',
@@ -45,6 +45,11 @@ export enum BuildEvent {
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
}
export enum MetadataEvent {
APPLE_METADATA_DOWNLOAD = 'metadata cli download apple response',
APPLE_METADATA_UPLOAD = 'metadata cli upload apple response',
}
export class Analytics {
static logEvent(name: Event, properties: Record<string, any>): void {
logEvent(name, properties);
@@ -105,7 +105,7 @@ export async function maybeResolveVersionsAsync(
appBuildVersion:
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
};
} catch (err: any) {
} catch {
return {};
}
} else {
+19 -11
View File
@@ -231,16 +231,23 @@ export async function waitForBuildEndAsync(
intervalSec = 10,
} = {}
): Promise<MaybeBuildFragment[]> {
const b = `build${buildIds.length > 1 ? 's' : ''}`;
Log.log(`Waiting for ${b} to complete. You can press Ctrl+C to exit.`);
const spinner = ora(`Waiting for ${b} to complete.`).start();
let spinner;
let originalSpinnerText;
if (buildIds.length === 1) {
Log.log('Waiting for build to complete. You can press Ctrl+C to exit.');
originalSpinnerText = 'Waiting for build to complete.';
spinner = ora(originalSpinnerText).start();
} else {
originalSpinnerText = 'Waiting for builds to complete. You can press Ctrl+C to exit.';
spinner = ora('Waiting for builds to complete. You can press Ctrl+C to exit.').start();
}
const endTime = new Date().getTime() + timeoutSec * 1000;
while (new Date().getTime() <= endTime) {
const builds = await getBuildsSafelyAsync(buildIds);
const { refetch } =
builds.length === 1
? await handleSingleBuildProgressAsync({ build: builds[0], accountName }, { spinner })
: await handleMultipleBuildsProgressAsync({ builds }, { spinner });
: await handleMultipleBuildsProgressAsync({ builds }, { spinner, originalSpinnerText });
if (!refetch) {
return builds;
}
@@ -373,7 +380,7 @@ const platforms = [AppPlatform.Android, AppPlatform.Ios];
async function handleMultipleBuildsProgressAsync(
{ builds: maybeBuilds }: { builds: MaybeBuildFragment[] },
{ spinner }: { spinner: Ora }
{ spinner, originalSpinnerText }: { spinner: Ora; originalSpinnerText: string }
): Promise<BuildProgressResult> {
const buildCount = maybeBuilds.length;
const builds = maybeBuilds.filter<BuildFragment>(isBuildFragment);
@@ -393,7 +400,7 @@ async function handleMultipleBuildsProgressAsync(
}
return { refetch: false };
} else {
spinner.text = formatPendingBuildsText(builds);
spinner.text = formatPendingBuildsText(originalSpinnerText, builds);
return { refetch: true };
}
}
@@ -412,9 +419,10 @@ function formatSettledBuildsText(builds: BuildFragment[]): string {
.join('\n ');
}
function formatPendingBuildsText(builds: BuildFragment[]): string {
return platforms
.map(platform => {
function formatPendingBuildsText(originalSpinnerText: string, builds: BuildFragment[]): string {
return [
originalSpinnerText,
...platforms.map(platform => {
const build = builds.find(build => build.platform === platform);
const status = build ? statusToDisplayName[build.status] : 'unknown';
let extraInfo = '';
@@ -437,8 +445,8 @@ function formatPendingBuildsText(builds: BuildFragment[]): string {
return `${appPlatformEmojis[platform]} ${
appPlatformDisplayNames[platform]
} build - status: ${chalk.bold(status)}${extraInfo}`;
})
.join('\n ');
}),
].join('\n ');
}
function isBuildFragment(maybeBuild: MaybeBuildFragment): maybeBuild is BuildFragment {
+6 -7
View File
@@ -36,13 +36,12 @@ export async function createIosContextAsync(
},
buildProfile
);
const targets = await resolveTargetsAsync(
{
projectDir: ctx.projectDir,
exp: ctx.exp,
},
xcodeBuildContext
);
const targets = await resolveTargetsAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
xcodeBuildContext,
env: buildProfile.env,
});
const applicationTarget = findApplicationTarget(targets);
const applicationTargetBuildSettings = resolveBuildSettings(ctx, applicationTarget);
@@ -2,7 +2,6 @@ import { Platform } from '@expo/eas-build-job';
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
import { resolveEntitlementsJsonAsync } from '../../credentials/ios/appstore/entitlements';
import { IosCredentials, Target } from '../../credentials/ios/types';
import { CredentialsResult } from '../build';
import { BuildContext } from '../context';
@@ -19,13 +18,6 @@ export async function ensureIosCredentialsAsync(
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
app: getAppFromContext(buildCtx.credentialsCtx),
targets,
iosCapabilitiesOptions: {
entitlements: await resolveEntitlementsJsonAsync(
buildCtx.projectDir,
buildCtx.workflow,
buildCtx.buildProfile.env ?? {}
),
},
distribution: buildCtx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
});
+1 -1
View File
@@ -6,7 +6,7 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.74';
const PLUGIN_PACKAGE_VERSION = '0.0.78';
export interface LocalBuildOptions {
enable: boolean;
@@ -1,12 +1,8 @@
import { asMock } from '../../__tests__/utils';
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
import { getUserAsync } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions';
import EasCommand from '../EasCommand';
import TestEasCommand from './TestEasCommand';
TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
jest.mock('../../user/User');
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
@@ -32,17 +28,30 @@ afterAll(() => {
});
beforeEach(() => {
asMock(getUserAsync)
jest
.mocked(getUserAsync)
.mockReset()
.mockImplementation(() => mockJester);
asMock(ensureLoggedInAsync)
.mockImplementation(async () => mockJester);
jest
.mocked(ensureLoggedInAsync)
.mockReset()
.mockImplementation(() => mockJester);
asMock(initAsync).mockReset();
asMock(flushAsync).mockReset();
asMock(logEvent).mockReset();
.mockImplementation(async () => mockJester);
jest.mocked(initAsync).mockReset();
jest.mocked(flushAsync).mockReset();
jest.mocked(logEvent).mockReset();
});
const createTestEasCommand = (authValue: boolean): typeof EasCommand => {
class TestEasCommand extends EasCommand {
requiresAuthentication = authValue;
async runAsync(): Promise<void> {}
}
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
return TestEasCommand;
};
describe(EasCommand.name, () => {
describe('without exceptions', () => {
// The first test in this suite should have an increased timeout
@@ -53,32 +62,36 @@ describe(EasCommand.name, () => {
// See https://github.com/oclif/command/blob/master/src/command.ts#L80
// and look for "Config.load"
it('ensures the user is logged in', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(ensureLoggedInAsync).toHaveBeenCalled();
}, 15_000);
it('ensures the user data is read from cache', async () => {
asMock(TestEasCommand.prototype.authValue).mockImplementationOnce(() => false);
const TestEasCommand = createTestEasCommand(false);
await TestEasCommand.run();
expect(getUserAsync).toHaveReturnedWith(mockJester);
expect(jest.mocked(getUserAsync)).toBeCalledTimes(1);
expect(jest.mocked(ensureLoggedInAsync)).not.toBeCalled();
});
it('initializes analytics', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(initAsync).toHaveBeenCalled();
});
it('flushes analytics', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(flushAsync).toHaveBeenCalled();
});
it('logs events', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(logEvent).toHaveBeenCalledWith('action', {
@@ -89,11 +102,12 @@ describe(EasCommand.name, () => {
describe('after exceptions', () => {
it('flushes analytics', async () => {
const TestEasCommand = createTestEasCommand(true);
try {
await TestEasCommand.run().then(() => {
throw new Error('foo');
});
} catch (error) {}
} catch {}
expect(flushAsync).toHaveBeenCalled();
});
@@ -1,13 +0,0 @@
import EasCommand from '../EasCommand';
export default class TestEasCommand extends EasCommand {
requiresAuthentication = this.authValue();
authValue(): boolean {
return false;
}
async runAsync(): Promise<void> {}
}
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
@@ -104,7 +104,7 @@ async function selectBuildToCancelAsync(
async function ensureBuildExistsAsync(buildId: string): Promise<void> {
try {
await BuildQuery.byIdAsync(buildId);
} catch (err) {
} catch {
throw new Error(`Couldn't find a build matching the id ${buildId}`);
}
}
@@ -102,7 +102,7 @@ export default class BuildInspect extends EasCommand {
if (!flags.verbose) {
Log.log(chalk.green('Build successful'));
}
} catch (err) {
} catch {
if (!flags.verbose) {
Log.error('Build failed');
Log.error(`Re-run this command with ${chalk.bold('--verbose')} flag to see the logs`);
@@ -45,7 +45,7 @@ export function getBranchMapping(branchMappingString?: string): {
let branchMapping: BranchMapping;
try {
branchMapping = JSON.parse(branchMappingString);
} catch (e) {
} catch {
throw new Error(`Could not parse branchMapping string into a JSON: "${branchMappingString}"`);
}
assert(branchMapping, 'Branch Mapping must be defined.');
+83
View File
@@ -0,0 +1,83 @@
import { getConfig } from '@expo/config';
import { Errors, Flags } from '@oclif/core';
import EasCommand from '../commandUtils/EasCommand';
import Log from '../log';
import { createMetadataContextAsync } from '../metadata/context';
import { handleMetadataError } from '../metadata/errors';
import { uploadMetadataAsync } from '../metadata/upload';
import { RequestedPlatform } from '../platform';
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
type RawCommandFlags = {
platform?: string;
profile?: string;
'non-interactive': boolean;
};
type CommandFlags = {
requestedPlatforms: RequestedPlatform;
profile?: string;
nonInteractive: boolean;
};
export default class Metadata extends EasCommand {
static description = 'upload metadata configuration to the app stores';
static flags = {
platform: Flags.enum({
char: 'p',
options: ['ios'],
}),
profile: Flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
'non-interactive': Flags.boolean({
default: false,
description: 'Run command in non-interactive mode',
}),
};
static args = [];
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(Metadata);
const flags = await this.sanitizeFlagsAsync(rawFlags);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
await getProjectIdAsync(exp);
const metadataContext = await createMetadataContextAsync({
projectDir,
exp,
profileName: flags.profile,
nonInteractive: flags.nonInteractive,
});
try {
Log.addNewLineIfNone();
await uploadMetadataAsync(metadataContext);
Log.addNewLineIfNone();
Log.succeed(`Store has been updated with your ${metadataContext.metadataFile} configuration`);
} catch (error: any) {
handleMetadataError(error);
}
}
private async sanitizeFlagsAsync(flags: RawCommandFlags): Promise<CommandFlags> {
const { platform, profile, 'non-interactive': nonInteractive } = flags;
if (!platform && nonInteractive) {
Errors.error('--platform is required when building in non-interactive mode', { exit: 1 });
}
return {
// TODO: add support for multiple platforms, right now we only support ios
requestedPlatforms: RequestedPlatform.Ios, // enforced by the flag options
profile,
nonInteractive,
};
}
}
@@ -0,0 +1,86 @@
import { getConfig } from '@expo/config';
import { Errors, Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import Log, { learnMore } from '../../log';
import { createMetadataContextAsync } from '../../metadata/context';
import { downloadMetadataAsync } from '../../metadata/download';
import { handleMetadataError } from '../../metadata/errors';
import { RequestedPlatform } from '../../platform';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
type RawCommandFlags = {
platform?: string;
profile?: string;
'non-interactive': boolean;
};
type CommandFlags = {
requestedPlatforms: RequestedPlatform;
profile?: string;
nonInteractive: boolean;
};
export default class MetadataConfigure extends EasCommand {
static description = 'upload metadata configuration to the app stores';
static flags = {
platform: Flags.enum({
char: 'p',
options: ['ios'],
}),
profile: Flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
'non-interactive': Flags.boolean({
default: false,
description: 'Run command in non-interactive mode',
}),
};
static args = [];
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(MetadataConfigure);
const flags = await this.sanitizeFlagsAsync(rawFlags);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
await getProjectIdAsync(exp);
const metadataContext = await createMetadataContextAsync({
projectDir,
exp,
profileName: flags.profile,
nonInteractive: flags.nonInteractive,
});
try {
Log.addNewLineIfNone();
const filePath = await downloadMetadataAsync(metadataContext);
Log.addNewLineIfNone();
Log.succeed('Your store configuration has been generated!');
Log.log(filePath);
Log.log(learnMore('https://docs.expo.dev/eas-metadata/introduction/'));
} catch (error: any) {
handleMetadataError(error);
}
}
private async sanitizeFlagsAsync(flags: RawCommandFlags): Promise<CommandFlags> {
const { platform, profile, 'non-interactive': nonInteractive } = flags;
if (!platform && nonInteractive) {
Errors.error('--platform is required when building in non-interactive mode', { exit: 1 });
}
return {
// TODO: add support for multiple platforms, right now we only support ios
requestedPlatforms: RequestedPlatform.Ios, // enforced by the flag options
profile,
nonInteractive,
};
}
}
@@ -76,7 +76,9 @@ export const testPushKey: ApplePushKeyFragment = {
iosAppCredentialsList: [],
};
export const testTargets: Target[] = [{ targetName: 'testapp', bundleIdentifier: 'foo.bar.com' }];
export const testTargets: Target[] = [
{ targetName: 'testapp', bundleIdentifier: 'foo.bar.com', entitlements: {} },
];
export const testProvisioningProfileFragment: AppleProvisioningProfileFragment = {
id: 'test-prov-prof-id-1',
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync, promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -17,7 +16,7 @@ import {
jest.mock('../../../../ora');
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe('BuildCredentialsUtils', () => {
describe(canCopyLegacyCredentialsAsync, () => {
@@ -84,7 +83,9 @@ describe('BuildCredentialsUtils', () => {
});
describe(promptUserAndCopyLegacyCredentialsAsync, () => {
it('copies all legacy credentials to EAS if the user is eligible', async () => {
asMock(promptAsync).mockImplementation(() => ({ providedName: 'test-provided-name' }));
jest
.mocked(promptAsync)
.mockImplementation(async () => ({ providedName: 'test-provided-name' }));
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -1,11 +1,10 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateFcm } from '../CreateFcm';
jest.mock('../../../../prompts');
asMock(promptAsync).mockImplementation(() => ({ fcmApiKey: 'blah' }));
jest.mocked(promptAsync).mockImplementation(async () => ({ fcmApiKey: 'blah' }));
describe(CreateFcm, () => {
it('creates an fcm api key in Interactive Mode', async () => {
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
@@ -8,7 +7,7 @@ import { CreateGoogleServiceAccountKey } from '../CreateGoogleServiceAccountKey'
jest.mock('../../../../prompts');
jest.mock('fs');
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
filePath: '/google-service-account-key.json',
}));
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { jester as mockJester } from '../../../../credentials/__tests__/fixtures-constants';
import { testKeystore } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -15,7 +14,7 @@ jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn(
describe('CreateKeystore', () => {
beforeEach(() => {
asMock(generateRandomKeystoreAsync).mockReset();
jest.mocked(generateRandomKeystoreAsync).mockReset();
});
it('creates a keystore in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
@@ -28,7 +27,7 @@ describe('CreateKeystore', () => {
expect(generateRandomKeystoreAsync).toHaveBeenCalled();
});
it('creates a keystore by uploading', async () => {
asMock(askForUserProvidedAsync).mockImplementationOnce(() => testKeystore);
jest.mocked(askForUserProvidedAsync).mockImplementationOnce(async () => testKeystore);
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -9,7 +8,7 @@ import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { RemoveFcm } from '../RemoveFcm';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(RemoveFcm, () => {
it('removes an FCM API Key', async () => {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -8,7 +7,7 @@ import { createCtxMock } from '../../../__tests__/fixtures-context';
import { RemoveGoogleServiceAccountKey } from '../RemoveGoogleServiceAccountKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(RemoveGoogleServiceAccountKey, () => {
it('removes an Google Service Account Key', async () => {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -7,7 +6,7 @@ import { RemoveKeystore } from '../RemoveKeystore';
jest.mock('fs-extra');
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(RemoveKeystore, () => {
it('removes a keystore', async () => {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import {
getNewAndroidApiMock,
testAndroidBuildCredentialsFragment,
@@ -19,7 +18,7 @@ jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn(
describe('SetUpBuildCredentials', () => {
beforeEach(() => {
asMock(generateRandomKeystoreAsync).mockReset();
jest.mocked(generateRandomKeystoreAsync).mockReset();
});
it('skips setup when there are prior credentials', async () => {
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { AndroidKeystoreType } from '../../../../graphql/generated';
import { confirmAsync } from '../../../../prompts';
import {
@@ -13,13 +12,14 @@ import {
SelectAndroidBuildCredentials,
SelectAndroidBuildCredentialsResultType,
} from '../../../manager/SelectAndroidBuildCredentials';
import { AppLookupParams } from '../../api/GraphqlClient';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetUpBuildCredentialsFromCredentialsJson } from '../SetUpBuildCredentialsFromCredentialsJson';
jest.mock('fs');
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
jest.mock('../../../manager/SelectAndroidBuildCredentials');
beforeEach(() => {
@@ -28,7 +28,7 @@ beforeEach(() => {
describe(SetUpBuildCredentialsFromCredentialsJson, () => {
it('sets up a new build configuration from credentials.json upon user request', async () => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
jest.mocked(SelectAndroidBuildCredentials).mockImplementation((() => {
return {
runAsync: () => {
return {
@@ -37,7 +37,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
};
},
};
});
}) as any as (app: AppLookupParams) => SelectAndroidBuildCredentials);
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -78,7 +78,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
expect(ctx.android.createAndroidAppBuildCredentialsAsync).toHaveBeenCalledTimes(1);
});
it('uses an existing build configuration upon user request', async () => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
jest.mocked(SelectAndroidBuildCredentials).mockImplementation((() => {
return {
runAsync: () => {
return {
@@ -87,7 +87,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
};
},
};
});
}) as any as (app: AppLookupParams) => SelectAndroidBuildCredentials);
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -14,7 +13,7 @@ import { SetUpGoogleServiceAccountKey } from '../SetUpGoogleServiceAccountKey';
jest.mock('../../../../prompts');
jest.mock('fs');
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
filePath: '/google-service-account-key.json',
}));
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -9,7 +8,9 @@ import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { UseExistingGoogleServiceAccountKey } from '../UseExistingGoogleServiceAccountKey';
jest.mock('../../../../prompts');
asMock(promptAsync).mockImplementation(() => ({ chosenKey: testGoogleServiceAccountKeyFragment }));
jest
.mocked(promptAsync)
.mockImplementation(async () => ({ chosenKey: testGoogleServiceAccountKeyFragment }));
describe(UseExistingGoogleServiceAccountKey, () => {
it('uses an existing Google Service Account Key in Interactive Mode', async () => {
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { detectGoogleServiceAccountKeyPathAsync } from '../googleServiceAccountKey';
@@ -25,13 +24,13 @@ afterAll(() => {
});
afterEach(() => {
asMock(promptAsync).mockClear();
asMock(confirmAsync).mockClear();
jest.mocked(promptAsync).mockClear();
jest.mocked(confirmAsync).mockClear();
});
describe('Google Service Account Key path detection', () => {
it('detects a single google-services file and prompts for confirmation', async () => {
asMock(confirmAsync).mockResolvedValueOnce(true);
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir/subdir');
expect(confirmAsync).toHaveBeenCalledWith(
expect.objectContaining({ message: 'Would you like to use this file?' })
@@ -46,7 +45,7 @@ describe('Google Service Account Key path detection', () => {
});
it('returns null, when user rejects to use detected file', async () => {
asMock(confirmAsync).mockResolvedValueOnce(false);
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir/subdir');
expect(confirmAsync).toHaveBeenCalledTimes(1);
expect(confirmAsync).toHaveBeenCalledWith(
@@ -56,7 +55,7 @@ describe('Google Service Account Key path detection', () => {
});
it('displays a chooser, when multiple files are found', async () => {
asMock(promptAsync).mockResolvedValueOnce({
jest.mocked(promptAsync).mockResolvedValueOnce({
selectedPath: '/project_dir/another-service-account.json',
});
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir'); // should find 2 files here
@@ -76,7 +75,7 @@ describe('Google Service Account Key path detection', () => {
});
it('returns null, when user selects the "None of above"', async () => {
asMock(promptAsync).mockResolvedValueOnce({
jest.mocked(promptAsync).mockResolvedValueOnce({
selectedPath: false,
});
@@ -1,6 +1,5 @@
import mockdate from 'mockdate';
import { asMock } from '../../../../__tests__/utils';
import Log from '../../../../log';
import {
testAndroidAppCredentialsFragment,
@@ -25,7 +24,9 @@ describe('print credentials', () => {
appLookupParams,
appCredentials: testAndroidAppCredentialsFragment,
});
const loggedSoFar = asMock(Log.log).mock.calls.reduce((acc, mockValue) => acc + mockValue);
const loggedSoFar = jest
.mocked(Log.log)
.mock.calls.reduce((acc, mockValue) => acc + mockValue.toString(), '');
expect(loggedSoFar).toMatchSnapshot();
});
});
@@ -20,7 +20,7 @@ function fileIsServiceAccountKey(keyJsonPath: string): boolean {
try {
readAndValidateServiceAccountKey(keyJsonPath);
return true;
} catch (err: any) {
} catch {
return false;
}
}
@@ -24,7 +24,7 @@ export async function keytoolCommandExistsAsync(): Promise<boolean> {
try {
await spawnAsync('keytool');
return true;
} catch (error) {
} catch {
return false;
}
}
@@ -24,7 +24,7 @@ function isPKCSKeystore(keystore: Keystore): boolean {
try {
parsePKCS12(keystore.keystore, keystore.keystorePassword);
return true;
} catch (e) {
} catch {
return false;
}
}
@@ -33,7 +33,7 @@ function isJKSKeystore(keystore: Keystore): boolean {
try {
jks.parseJks(Buffer.from(keystore.keystore, 'base64'), keystore.keystorePassword);
return true;
} catch (e) {
} catch {
return false;
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ export class CredentialsContext {
): ExpoConfig | null {
try {
return getExpoConfig(projectDir, { env });
} catch (error) {
} catch {
// ignore error, context might be created outside of expo project
return null;
}
@@ -2,7 +2,6 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import prompts from 'prompts';
import { asMock } from '../../../__tests__/utils';
import { IosDistributionType } from '../../../graphql/generated';
import {
testKeystore,
@@ -25,8 +24,8 @@ jest.mock('prompts');
describe('update credentials.json', () => {
beforeEach(() => {
vol.reset();
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
jest.mocked(prompts).mockReset();
jest.mocked(prompts).mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
});
@@ -176,6 +175,7 @@ describe('update credentials.json', () => {
{
targetName: 'testapp',
bundleIdentifier: 'com.bundle.id',
entitlements: {},
},
];
@@ -114,7 +114,7 @@ export async function readRawAsync(
try {
const credentialsJSONContents = await fs.readFile(credentialsJsonFilePath, 'utf8');
return JSON.parse(credentialsJSONContents);
} catch (err) {
} catch {
throw new Error(
`credentials.json must exist in the project root directory and contain a valid JSON`
);
@@ -11,7 +11,6 @@ import { ensureAllTargetsAreConfigured } from '../credentialsJson/utils';
import { getAppFromContext } from './actions/BuildCredentialsUtils';
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
import { SetUpPushKey } from './actions/SetUpPushKey';
import { IosCapabilitiesOptions } from './appstore/ensureAppExists';
import { App, IosCredentials, Target } from './types';
import { isAdHocProfile } from './utils/provisioningProfile';
@@ -20,7 +19,6 @@ interface Options {
targets: Target[];
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
export default class IosCredentialsProvider {
@@ -63,7 +61,6 @@ export default class IosCredentialsProvider {
targets: this.options.targets,
distribution: this.options.distribution,
enterpriseProvisioning: this.options.enterpriseProvisioning,
iosCapabilitiesOptions: this.options.iosCapabilitiesOptions,
}).runAsync(this.ctx);
}
@@ -58,7 +58,13 @@ describe(IosCredentialsProvider, () => {
},
projectName: appLookupParams.projectName,
},
targets: [{ targetName: 'testapp', bundleIdentifier: appLookupParams.bundleIdentifier }],
targets: [
{
targetName: 'testapp',
bundleIdentifier: appLookupParams.bundleIdentifier,
entitlements: {},
},
],
distribution: 'store',
});
await expect(provider.getCredentialsAsync(CredentialsSource.REMOTE)).rejects.toThrowError(
@@ -95,7 +101,13 @@ describe(IosCredentialsProvider, () => {
},
projectName: appLookupParams.projectName,
},
targets: [{ targetName: 'testapp', bundleIdentifier: appLookupParams.bundleIdentifier }],
targets: [
{
targetName: 'testapp',
bundleIdentifier: appLookupParams.bundleIdentifier,
entitlements: {},
},
],
distribution: 'store',
});
@@ -4,7 +4,6 @@ import nullthrows from 'nullthrows';
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
import { App, IosAppBuildCredentialsMap, IosCredentials, Target } from '../types';
import { displayProjectCredentials } from '../utils/printCredentials';
import { SetUpTargetBuildCredentials } from './SetUpTargetBuildCredentials';
@@ -14,7 +13,6 @@ interface Options {
targets: Target[];
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
export class SetUpBuildCredentials {
@@ -50,7 +48,9 @@ export class SetUpBuildCredentials {
Log.newLine();
}
const action = new SetUpTargetBuildCredentials({
...this.options,
enterpriseProvisioning: this.options.enterpriseProvisioning,
distribution: this.options.distribution,
entitlements: target.entitlements,
app: {
...this.options.app,
bundleIdentifier: target.bundleIdentifier,
@@ -1,4 +1,5 @@
import { DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import { JSONObject } from '@expo/json-file';
import {
IosDistributionType as GraphQLIosDistributionType,
@@ -7,7 +8,6 @@ import {
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { AppLookupParams as GraphQLAppLookupParams } from '../api/GraphqlClient';
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
import { SetUpAdhocProvisioningProfile } from './SetUpAdhocProvisioningProfile';
import { SetUpInternalProvisioningProfile } from './SetUpInternalProvisioningProfile';
import { SetUpProvisioningProfile } from './SetUpProvisioningProfile';
@@ -16,13 +16,13 @@ interface Options {
app: GraphQLAppLookupParams;
distribution: DistributionType;
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
entitlements: JSONObject;
}
export class SetUpTargetBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
const { app, iosCapabilitiesOptions } = this.options;
const { app, entitlements } = this.options;
await ctx.bestEffortAppStoreAuthenticateAsync();
@@ -33,7 +33,7 @@ export class SetUpTargetBuildCredentials {
bundleIdentifier: app.bundleIdentifier,
projectName: app.projectName,
},
iosCapabilitiesOptions
{ entitlements }
);
}
try {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -18,8 +17,8 @@ function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
}
afterEach(() => {
asMock(promptAsync).mockClear();
asMock(getCredentialsFromUserAsync).mockClear();
jest.mocked(promptAsync).mockClear();
jest.mocked(getCredentialsFromUserAsync).mockClear();
});
describe(getAscApiKeyName, () => {
@@ -35,10 +34,10 @@ describe(getAscApiKeyName, () => {
describe(promptForAscApiKeyPathAsync, () => {
it('prompts for keyId, keyP8Path and issuerId when user is not authenticated to Apple', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
jest.mocked(getCredentialsFromUserAsync).mockImplementation(async () => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
@@ -62,10 +61,10 @@ describe(promptForAscApiKeyPathAsync, () => {
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(2); // keyId, issuerId
});
it('prompts for keyId, keyP8Path and detects issuerId when user is authenticated to Apple', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
jest.mocked(getCredentialsFromUserAsync).mockImplementation(async () => ({
keyId: 'test-key-id',
}));
const ctx = createCtxMock({
@@ -85,6 +84,6 @@ describe(promptForAscApiKeyPathAsync, () => {
});
expect(promptAsync).toHaveBeenCalledTimes(1); // keyP8Path
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(1); // keyId
expect(asMock(ctx.appStore.getAscApiKeyAsync).mock.calls.length).toBe(1); // issuerId
expect(jest.mocked(ctx.appStore.getAscApiKeyAsync).mock.calls.length).toBe(1); // issuerId
});
});
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -15,7 +14,7 @@ import { ConfigureProvisioningProfile } from '../ConfigureProvisioningProfile';
jest.mock('../../../../ora');
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe('ConfigureProvisioningProfile', () => {
it('configures a Provisioning Profile in Interactive Mode', async () => {
@@ -43,9 +42,9 @@ describe('ConfigureProvisioningProfile', () => {
await provProfConfigurator.runAsync(ctx);
// expect provisioning profile not to be updated on expo servers
expect(asMock(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(1);
// expect provisioning profile not to be updated on apple portal
expect(asMock(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -62,8 +61,8 @@ describe('ConfigureProvisioningProfile', () => {
);
// expect provisioning profile not to be updated on expo servers
expect(asMock(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be updated on apple portal
expect(asMock(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(0);
});
});
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -9,7 +8,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreateAscApiKey } from '../CreateAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(CreateAscApiKey, () => {
it('creates a App Store API Key in Interactive Mode', async () => {
@@ -27,9 +26,9 @@ describe(CreateAscApiKey, () => {
await createAscApiKeyAction.runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
// expect api key to be created on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
// expect api key to be created on apple portal
expect(asMock(ctx.appStore.createAscApiKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.createAscApiKeyAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -13,7 +12,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreateProvisioningProfile } from '../CreateProvisioningProfile';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe('CreateProvisioningProfile', () => {
it('creates a Provisioning Profile in Interactive Mode', async () => {
@@ -34,9 +33,9 @@ describe('CreateProvisioningProfile', () => {
await createProvProfAction.runAsync(ctx);
// expect provisioning profile to be created on expo servers
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be created on apple portal
expect(asMock(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -52,8 +51,8 @@ describe('CreateProvisioningProfile', () => {
);
// expect provisioning profile not to be created on expo servers
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be created on apple portal
expect(asMock(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(0);
});
});
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -8,7 +7,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreatePushKey } from '../CreatePushKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(CreatePushKey, () => {
it('creates a Push Key in Interactive Mode', async () => {
@@ -25,9 +24,9 @@ describe(CreatePushKey, () => {
await createPushKeyAction.runAsync(ctx);
// expect push key to be created on expo servers
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(1);
// expect push key to be created on apple portal
expect(asMock(ctx.appStore.createPushKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.createPushKeyAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,11 +1,10 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKeyFragment } from '../../../__tests__/fixtures-ios';
import { RemoveAscApiKey } from '../RemoveAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(RemoveAscApiKey, () => {
it('removes an Asc API Key', async () => {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -11,7 +10,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { RemoveDistributionCertificate } from '../RemoveDistributionCertificate';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe('RemoveDistributionCertificate', () => {
it('deletes the distribution certificate on Expo and Apple servers when there are no App Dependencies in Interactive Mode', async () => {
@@ -24,11 +23,11 @@ describe('RemoveDistributionCertificate', () => {
await removeDistCertAction.runAsync(ctx);
// expect dist cert to be deleted on expo servers
expect(asMock(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect dist cert to be deleted on apple portal
expect(asMock(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile deletion to be skipped because there arent any associated with the dist cert
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('deletes the distribution certificate on Expo servers when there are no App Dependencies in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -40,11 +39,11 @@ describe('RemoveDistributionCertificate', () => {
await removeDistCertAction.runAsync(ctx);
// expect dist cert to be deleted on expo servers
expect(asMock(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
// not supported in non-interactive mode
expect(asMock(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(0);
// expect provisioning profile deletion to be skipped because there arent any associated with the dist cert
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('deletes the distribution certificate and its provisioning profile on Expo and Apple servers when there are App Dependencies in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
@@ -56,11 +55,11 @@ describe('RemoveDistributionCertificate', () => {
await removeDistCertAction.runAsync(ctx);
// expect dist cert to be deleted on expo servers
expect(asMock(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect dist cert to be deleted on apple portal
expect(asMock(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile deletion to be invoked on expo servers
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
});
it('errors when the distribution certificate has App Dependencies in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testTargets } from '../../../__tests__/fixtures-ios';
@@ -20,6 +19,6 @@ describe('RemoveProvisioningProfile', () => {
await removeProfilesAction.runAsync(ctx);
// expect provisioning profile to be deleted from servers
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
});
});
@@ -1,11 +1,10 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testPushKey } from '../../../__tests__/fixtures-ios';
import { RemovePushKey } from '../RemovePushKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(RemovePushKey, () => {
it('deletes the push key on Expo and Apple servers in Interactive Mode', async () => {
@@ -14,9 +13,9 @@ describe(RemovePushKey, () => {
await removePushKeyAction.runAsync(ctx);
// expect push key to be deleted on expo servers
expect(asMock(ctx.ios.deletePushKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.deletePushKeyAsync).mock.calls.length).toBe(1);
// expect push key to be revoked on apple portal
expect(asMock(ctx.appStore.revokePushKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.revokePushKeyAsync).mock.calls.length).toBe(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -15,7 +14,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetUpAscApiKey, SetupAscApiKeyChoice } from '../SetUpAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(SetUpAscApiKey, () => {
it('skips setting up a ASC API Key if it is already configured', async () => {
@@ -36,9 +35,9 @@ describe(SetUpAscApiKey, () => {
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration not to be updated with a new ASC API Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
});
it('sets up a ASC API Key, creating a new one if there are no existing ASC API Keys', async () => {
const ctx = createCtxMock({
@@ -61,9 +60,9 @@ describe(SetUpAscApiKey, () => {
await setupAscApiKeyAction.runAsync(ctx);
// expect to create a new ASC API Key on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
// expect configuration to be updated with a new ASC API Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a ASC API Key, use autoselected ASC API Key when there are existing keys', async () => {
const ctx = createCtxMock({
@@ -87,9 +86,9 @@ describe(SetUpAscApiKey, () => {
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers, we are using the existing one
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration to be updated with a new ASC API Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a ASC API Key, allowing user to choose existing keys', async () => {
const ctx = createCtxMock({
@@ -99,7 +98,7 @@ describe(SetUpAscApiKey, () => {
getAscApiKeysForAccountAsync: jest.fn(() => [testAscApiKeyFragment]),
},
});
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
choice: SetupAscApiKeyChoice.USE_EXISTING,
chosenAscApiKey: testAscApiKeyFragment,
}));
@@ -111,9 +110,9 @@ describe(SetUpAscApiKey, () => {
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers, we are using the existing one
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration to be updated with a new ASC API Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('works in Non Interactive Mode if ASC Key is configured', async () => {
const ctx = createCtxMock({
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { IosAppBuildCredentialsFragment, IosDistributionType } from '../../../../graphql/generated';
import { promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -14,10 +13,10 @@ jest.mock('../SetUpProvisioningProfile');
jest.mock('../BuildCredentialsUtils', () => ({ getAllBuildCredentialsAsync: jest.fn() }));
beforeEach(() => {
asMock(promptAsync).mockReset();
jest.mocked(promptAsync).mockReset();
asMock(getAllBuildCredentialsAsync).mockReset();
asMock(getAllBuildCredentialsAsync).mockImplementation(() => {
jest.mocked(getAllBuildCredentialsAsync).mockReset();
jest.mocked(getAllBuildCredentialsAsync).mockImplementation(() => {
throw new Error(
`unhandled getAllBuildCredentialsAsync call - this shouldn't happen - fix tests!`
);
@@ -37,7 +36,7 @@ describe(SetUpInternalProvisioningProfile, () => {
describe('interactive mode', () => {
describe('when authenticated with apple', () => {
it('runs the SetUpAdhocProvisioningProfile action for non-enterprise team', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
@@ -64,10 +63,10 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('asks the user for an action to run when they have access to an enterprise team', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
distributionType: IosDistributionType.Enterprise,
}));
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
@@ -99,10 +98,10 @@ describe(SetUpInternalProvisioningProfile, () => {
});
describe('when not authenticated with apple', () => {
it('asks the user for an action to run when both adhoc and universal distribution credentials exist', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
distributionType: IosDistributionType.Enterprise,
}));
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [
testAdhocBuildCredentials,
testEnterpriseBuildCredentials,
@@ -133,10 +132,10 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('runs the SetUpAdhocProvisioningProfile action when adhoc credentials exist', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
distributionType: IosDistributionType.Enterprise,
}));
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [testAdhocBuildCredentials];
return buildCredentials;
});
@@ -164,10 +163,10 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('runs the SetUpProvisioningProfile action when enterprise credentials exist', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
distributionType: IosDistributionType.Enterprise,
}));
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [
testEnterpriseBuildCredentials,
];
@@ -197,10 +196,10 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('forces the apple authentication when neither adhoc nor enterprise credentials exist', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
distributionType: IosDistributionType.Enterprise,
}));
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
@@ -228,7 +227,7 @@ describe(SetUpInternalProvisioningProfile, () => {
describe('non-interactive mode', () => {
it('throws an error when both adhoc and enterprise credentials are set up', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [
testAdhocBuildCredentials,
testEnterpriseBuildCredentials,
@@ -249,7 +248,7 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('throws an error when neither adhoc nor enterprise credentials are set up', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
@@ -265,7 +264,7 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('runs the SetUpAdhocProvisioningProfile action when adhoc credentials exist', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [testAdhocBuildCredentials];
return buildCredentials;
});
@@ -287,7 +286,7 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('runs the SetUpProvisioningProfile action when enterprise credentials exist', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [testEnterpriseBuildCredentials];
return buildCredentials;
});
@@ -1,6 +1,5 @@
import nullthrows from 'nullthrows';
import { asMock } from '../../../../__tests__/utils';
import { IosDistributionType } from '../../../../graphql/generated';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
@@ -19,7 +18,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetUpProvisioningProfile } from '../SetUpProvisioningProfile';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
jest.mock('../SetUpDistributionCertificate');
jest.mock('../ConfigureProvisioningProfile');
jest.mock('../CreateProvisioningProfile');
@@ -27,7 +26,7 @@ jest.mock('../../validators/validateProvisioningProfile');
describe('SetUpProvisioningProfile', () => {
it('repairs existing Provisioning Profile with bad build credentials in Interactive Mode', async () => {
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -61,12 +60,14 @@ describe('SetUpProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile not to be deleted on expo servers
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('sets up a new Provisioning Profile with bad build credentials in Interactive Mode', async () => {
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -91,12 +92,14 @@ describe('SetUpProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile to be deleted on expo servers
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
});
it('skips setting up a Provisioning Profile with prior build credentials configured properly in Interactive Mode', async () => {
asMock(validateProvisioningProfileAsync).mockImplementation(() => true);
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => true);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -121,12 +124,14 @@ describe('SetUpProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
0
);
// expect provisioning profile not to be deleted on expo servers
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('sets up a Provisioning Profile with no prior build credentials configured in Interactive Mode', async () => {
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -147,12 +152,14 @@ describe('SetUpProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile not to be deleted on expo servers
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('errors in Non Interactive Mode', async () => {
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: true,
});
@@ -166,8 +173,10 @@ describe('SetUpProvisioningProfile', () => {
);
// expect build credentials not to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
0
);
// expect provisioning profile not to be deleted on expo servers
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
});
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -8,7 +7,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetUpPushKey } from '../SetUpPushKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(SetUpPushKey, () => {
it('skips setting up a Push Key if it is already configured', async () => {
@@ -24,9 +23,9 @@ describe(SetUpPushKey, () => {
await setupPushKeyAction.runAsync(ctx);
// expect not to create a new push key on expo servers
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(0);
// expect configuration not to be updated with a new push key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
});
it('sets up a Push Key, creating a new one if there are no existing push keys', async () => {
const ctx = createCtxMock({
@@ -42,9 +41,9 @@ describe(SetUpPushKey, () => {
await setupPushKeyAction.runAsync(ctx);
// expect to create a new push key on expo servers
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(1);
// expect configuration to be updated with a new push key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a Push Key, use autoselected push key when there are existing keys', async () => {
const ctx = createCtxMock({
@@ -66,9 +65,9 @@ describe(SetUpPushKey, () => {
await setupPushKeyAction.runAsync(ctx);
// expect not to create a new push key on expo servers, we are using the existing one
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(0);
// expect configuration not to be updated with a new push key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -15,15 +14,16 @@ import {
} from '../SetUpSubmissionCredentials';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(SetUpSubmissionCredentials, () => {
it('allows user to enter an App Specific Password', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({
choice: PROMPT_FOR_APP_SPECIFIC_PASSWORD,
}))
.mockImplementationOnce(() => ({
.mockImplementationOnce(async () => ({
appSpecificPassword: 'super secret',
}));
const ctx = createCtxMock({
@@ -37,7 +37,7 @@ describe(SetUpSubmissionCredentials, () => {
const asp = await setupAscApiKeyAction.runAsync(ctx);
// prompt to choose ASP, then prompt to input ASP
expect(asMock(promptAsync).mock.calls.length).toBe(2);
expect(jest.mocked(promptAsync).mock.calls.length).toBe(2);
expect(asp).toBe('super secret');
});
it('returns an ASC API key', async () => {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { IosDistributionType } from '../../../../graphql/generated';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
@@ -21,8 +20,8 @@ jest.mock('../../../../prompts');
jest.mock('../../../credentialsJson/read');
beforeEach(() => {
asMock(confirmAsync).mockReset();
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockReset();
jest.mocked(confirmAsync).mockImplementation(async () => true);
});
describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
@@ -61,11 +60,13 @@ describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
0
);
// expect distribution certificate not to be uploaded on expo servers
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be uploaded on expo servers
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
});
it('sets up build credentials with no prior configuration in Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -90,11 +91,13 @@ describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect distribution certificate to be uploaded on expo servers
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be uploaded on expo servers
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('sets up build credentials with different prior configuration in Interactive Mode', async () => {
// create deep clone the quick and dirty way
@@ -123,11 +126,13 @@ describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect distribution certificate to be uploaded on expo servers
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be uploaded on expo servers
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('works in Non Interactive Mode - sets up build credentials with different prior configuration', async () => {
// create deep clone the quick and dirty way
@@ -157,12 +162,14 @@ describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect no prompts to be called
expect(asMock(confirmAsync).mock.calls.length).toBe(0);
expect(jest.mocked(confirmAsync).mock.calls.length).toBe(0);
// expect build credentials to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect distribution certificate to be uploaded on expo servers
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be uploaded on expo servers
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
});
@@ -1,6 +1,5 @@
import { ApiKey, ApiKeyType, UserRole } from '@expo/apple-utils';
import { asMock } from '../../../../__tests__/utils';
import {
createAscApiKeyAsync,
getAscApiKeyAsync,
@@ -57,7 +56,7 @@ beforeEach(() => {
test(`listAscApiKeysAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'getAsync').mockImplementation(async () => []);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await listAscApiKeysAsync(mockAuthCtx);
expect(ApiKey.getAsync).toHaveBeenLastCalledWith(mockRequestContext);
expect(result).toEqual([]);
@@ -66,7 +65,7 @@ test(`listAscApiKeysAsync`, async () => {
test(`getAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'infoAsync').mockImplementation(async () => mockApiKey);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await getAscApiKeyAsync(mockAuthCtx, 'test-key-id');
expect(ApiKey.infoAsync).toHaveBeenLastCalledWith(mockRequestContext, { id: 'test-key-id' });
expect(result).toEqual(mockAscApiKeyInfo);
@@ -75,7 +74,7 @@ test(`getAscApiKeyAsync`, async () => {
test(`createAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'createAsync').mockImplementation(async () => mockApiKey);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await createAscApiKeyAsync(mockAuthCtx, { nickname: 'test-name' });
expect(ApiKey.createAsync).toHaveBeenLastCalledWith(mockRequestContext, {
nickname: 'test-name',
@@ -89,7 +88,7 @@ test(`createAscApiKeyAsync`, async () => {
test(`revokeAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'infoAsync').mockImplementation(async () => mockApiKey);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await revokeAscApiKeyAsync(mockAuthCtx, 'test-key-id');
expect(ApiKey.infoAsync).toHaveBeenLastCalledWith(mockRequestContext, {
id: 'test-key-id',
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { AppleTeamType } from '../authenticateTypes';
import { resolveAppleTeamAsync, resolveAscApiKeyAsync } from '../resolveCredentials';
@@ -17,7 +16,7 @@ const testAscApiKey = {
beforeEach(() => {
vol.reset();
process.env = {};
asMock(promptAsync).mockReset();
jest.mocked(promptAsync).mockReset();
});
describe(resolveAscApiKeyAsync, () => {
@@ -44,7 +43,7 @@ describe(resolveAscApiKeyAsync, () => {
vol.fromJSON({
'/test-asc-key.p8': testAscApiKey.keyP8,
});
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
ascApiKeyPath: '/test-asc-key.p8',
ascIssuerId: testAscApiKey.issuerId,
ascApiKeyId: testAscApiKey.keyId,
@@ -78,7 +77,7 @@ describe(resolveAppleTeamAsync, () => {
});
});
it(`prompts the user if it can't find anything else`, async () => {
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
appleTeamId: testTeam.teamId,
appleTeamType: testTeam.teamType,
}));
@@ -1,3 +1,5 @@
import { JSONObject } from '@expo/json-file';
import {
CommonIosAppCredentialsFragment,
IosAppBuildCredentialsFragment,
@@ -14,6 +16,7 @@ export interface Target {
buildConfiguration?: string;
bundleIdentifier: string;
parentBundleIdentifier?: string;
entitlements: JSONObject;
}
export interface TargetCredentials {
@@ -1,8 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`print credentials prints the IosAppCredentials map 1`] = `
Array [
"iOS Credentials
"iOS Credentials
Project @quinlanj/test52
Bundle Identifier com.quinlanj.test52
Apple Team test-apple-identifier
@@ -51,6 +50,5 @@ Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
",
]
"
`;
@@ -15,7 +15,7 @@ describe(getCertData, () => {
});
});
describe(forge.pkcs12.pkcs12FromAsn1, () => {
describe('forge.pkcs12.pkcs12FromAsn1', () => {
describe('when using p12 created from keychain', () => {
it('throws if empty string is passed', async () => {
const p12Der = forge.util.decode64(testDistributionCertificateEmptyPasswordBase64);
@@ -1,7 +1,6 @@
import mockdate from 'mockdate';
import nullthrows from 'nullthrows';
import { asMock } from '../../../../__tests__/utils';
import Log from '../../../../log';
import { IosAppCredentialsQuery } from '../../api/graphql/queries/IosAppCredentialsQuery';
import { App, Target } from '../../types';
@@ -29,9 +28,13 @@ describe('print credentials', () => {
const appCredentials = {
test52: nullthrows(testIosAppCredentialsData),
};
const targets: Target[] = [{ targetName: 'test52', bundleIdentifier: 'com.quinlanj.test52' }];
const targets: Target[] = [
{ targetName: 'test52', bundleIdentifier: 'com.quinlanj.test52', entitlements: {} },
];
displayIosCredentials(app, appCredentials, targets);
const loggedSoFar = asMock(Log.log).mock.calls.reduce((acc, mockValue) => acc + mockValue);
const loggedSoFar = jest
.mocked(Log.log)
.mock.calls.reduce((acc, mockValue) => acc + mockValue, '');
expect(loggedSoFar).toMatchSnapshot();
});
});
@@ -123,7 +123,7 @@ export function displayIosCredentials(
export function displayProjectCredentials(
app: App,
appBuildCredentials: IosAppBuildCredentialsMap,
targets: Target[]
targets: Omit<Target, 'entitlements'>[]
): void {
const projectFullName = `@${app.account.name}/${app.projectName}`;
const targetToBundleId = targets.reduce<Record<string, string>>((acc, target) => {
@@ -31,7 +31,7 @@ export function parse(dataBase64: string): PlistObject {
const buffer = Buffer.from(dataBase64, 'base64');
const profile = buffer.toString('utf8');
return plist.parse(profile) as PlistObject;
} catch (error) {
} catch {
throw new Error('Provisioning profile is malformed');
}
}
@@ -75,7 +75,7 @@ function validateProvisioningProfileWithoutApple(
Log.warn('Provisioning Profile has expired.');
return false;
}
} catch (error) {
} catch {
Log.warn('Provisioning Profile is malformed.');
return false;
}
@@ -43,7 +43,6 @@ import { SetUpProvisioningProfile } from '../ios/actions/SetUpProvisioningProfil
import { SetUpPushKey } from '../ios/actions/SetUpPushKey';
import { UpdateCredentialsJson } from '../ios/actions/UpdateCredentialsJson';
import { AppLookupParams } from '../ios/api/GraphqlClient';
import { getManagedEntitlementsJsonAsync } from '../ios/appstore/entitlements';
import { App, IosAppCredentialsMap, Target } from '../ios/types';
import { displayIosCredentials } from '../ios/utils/printCredentials';
import { ActionInfo, IosActionType, Scope } from './Actions';
@@ -192,10 +191,12 @@ export class ManageIos {
},
buildProfile
);
const targets = await resolveTargetsAsync(
{ exp: ctx.exp, projectDir: ctx.projectDir },
xcodeBuildContext
);
const targets = await resolveTargetsAsync({
exp: ctx.exp,
projectDir: ctx.projectDir,
xcodeBuildContext,
env: buildProfile.env,
});
return {
app,
targets,
@@ -235,12 +236,6 @@ export class ManageIos {
targets,
distribution: buildProfile.distribution,
enterpriseProvisioning: buildProfile.enterpriseProvisioning,
iosCapabilitiesOptions: {
entitlements: await getManagedEntitlementsJsonAsync(
ctx.projectDir,
buildProfile.env ?? {}
),
},
}).runAsync(ctx);
return;
}
@@ -1,4 +1,3 @@
import { asMock } from '../../../__tests__/utils';
import { promptAsync } from '../../../prompts';
import {
getNewAndroidApiMock,
@@ -15,12 +14,12 @@ const TEST_STRING = 'TEST_STRING';
jest.mock('../../../prompts');
beforeEach(() => {
asMock(promptAsync).mockReset();
jest.mocked(promptAsync).mockReset();
});
describe(SelectAndroidBuildCredentials, () => {
it('returns a request to make default build credentials when there are no credentials', async () => {
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
providedName: TEST_STRING,
}));
const ctx = createCtxMock({
@@ -38,7 +37,7 @@ describe(SelectAndroidBuildCredentials, () => {
});
});
it('returns a request to make build credentials when the user chooses to make a new one', async () => {
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
buildCredentialsResultOrRequestToCreateNew:
SelectAndroidBuildCredentialsResultType.CREATE_REQUEST,
providedName: TEST_STRING,
@@ -60,7 +59,7 @@ describe(SelectAndroidBuildCredentials, () => {
});
});
it('returns a request to make default build credentials when the user chooses to make a new one, and if they have no existing credentials', async () => {
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
buildCredentialsResultOrRequestToCreateNew:
SelectAndroidBuildCredentialsResultType.CREATE_REQUEST,
providedName: TEST_STRING,
@@ -80,7 +79,7 @@ describe(SelectAndroidBuildCredentials, () => {
});
});
it('returns buildCredentials of the users choice', async () => {
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
buildCredentialsResultOrRequestToCreateNew: testAndroidBuildCredentialsFragment,
}));
const ctx = createCtxMock({
@@ -1,6 +1,5 @@
import prompts from 'prompts';
import { asMock } from '../../__tests__/utils';
import Log from '../../log';
import { Actor } from '../../user/User';
import { AccountResolver } from '../manager';
@@ -15,8 +14,8 @@ jest.mock('../../project/projectUtils', () => {
jest.mock('../../credentials/ios/api/graphql/queries/AppleTeamQuery');
beforeEach(() => {
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
jest.mocked(prompts).mockReset();
jest.mocked(prompts).mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
});
@@ -38,7 +37,7 @@ describe(AccountResolver, () => {
const exp = {} as any;
it('returns the account defined in app.json/app.config.js if user confirms', async () => {
asMock(prompts).mockImplementationOnce(() => ({
jest.mocked(prompts).mockImplementationOnce(async () => ({
value: true,
}));
@@ -48,10 +47,10 @@ describe(AccountResolver, () => {
});
it('asks the user to choose the account from his account list if he rejects to use the one defined in app.json / app.config.js', async () => {
asMock(prompts).mockImplementationOnce(() => ({
jest.mocked(prompts).mockImplementationOnce(async () => ({
useProjectAccount: false,
}));
asMock(prompts).mockImplementationOnce(() => ({
jest.mocked(prompts).mockImplementationOnce(async () => ({
account: user.accounts[0],
}));
@@ -61,7 +60,7 @@ describe(AccountResolver, () => {
});
it(`asks the user to choose the account from his account list if he doesn't have access to the account defined in app.json / app.config.js`, async () => {
asMock(prompts).mockImplementationOnce(() => ({
jest.mocked(prompts).mockImplementationOnce(async () => ({
account: user.accounts[0],
}));
@@ -80,7 +79,7 @@ describe(AccountResolver, () => {
describe('when outside project dir', () => {
it('asks the user to choose the account from his account list', async () => {
asMock(prompts).mockImplementationOnce(() => ({
jest.mocked(prompts).mockImplementationOnce(async () => ({
account: user.accounts[0],
}));
@@ -1,6 +1,5 @@
import prompts from 'prompts';
import { asMock } from '../../../../__tests__/utils';
import { Account } from '../../../../user/Account';
import DeviceCreateAction, { RegistrationMethod } from '../action';
import { runInputMethodAsync } from '../inputMethod';
@@ -11,19 +10,19 @@ jest.mock('../registrationUrlMethod');
jest.mock('../inputMethod');
beforeEach(() => {
const promptsMock = asMock(prompts);
const promptsMock = jest.mocked(prompts);
promptsMock.mockReset();
promptsMock.mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
asMock(runRegistrationUrlMethodAsync).mockClear();
asMock(runInputMethodAsync).mockClear();
jest.mocked(runRegistrationUrlMethodAsync).mockClear();
jest.mocked(runInputMethodAsync).mockClear();
});
describe(DeviceCreateAction, () => {
describe('#runAsync', () => {
it('calls runRegistrationUrlMethodAsync if user chooses the website method', async () => {
asMock(prompts).mockImplementationOnce(() => ({
jest.mocked(prompts).mockImplementationOnce(async () => ({
method: RegistrationMethod.WEBSITE,
}));
@@ -43,7 +42,7 @@ describe(DeviceCreateAction, () => {
});
it('calls runInputMethodAsync if user chooses the input method', async () => {
asMock(prompts).mockImplementationOnce(() => ({
jest.mocked(prompts).mockImplementationOnce(async () => ({
method: RegistrationMethod.INPUT,
}));
@@ -1,6 +1,5 @@
import prompts from 'prompts';
import { asMock } from '../../../../__tests__/utils';
import { AppleDeviceMutation } from '../../../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
import { AppleDeviceClass, AppleTeam } from '../../../../graphql/generated';
import { runInputMethodAsync } from '../inputMethod';
@@ -9,19 +8,19 @@ jest.mock('../../../../credentials/ios/api/graphql/mutations/AppleDeviceMutation
jest.mock('../../../../ora');
beforeEach(() => {
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
jest.mocked(prompts).mockReset();
jest.mocked(prompts).mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
asMock(AppleDeviceMutation.createAppleDeviceAsync).mockClear();
jest.mocked(AppleDeviceMutation.createAppleDeviceAsync).mockClear();
});
describe(runInputMethodAsync, () => {
it('should allow for multiple device registration', async () => {
mockDeviceData('00001111-001122334455662E', 'my iPhone', AppleDeviceClass.Iphone);
asMock(prompts).mockImplementationOnce(() => ({ value: true }));
jest.mocked(prompts).mockImplementationOnce(async () => ({ value: true }));
mockDeviceData('b12cba9856d89c932ab7a4b813c4d932534e1679', 'my iPad', AppleDeviceClass.Ipad);
asMock(prompts).mockImplementationOnce(() => ({ value: false }));
jest.mocked(prompts).mockImplementationOnce(async () => ({ value: false }));
const accountId = 'account-id';
// @ts-expect-error appleTeam is missing properties of AppleTeam GraphQL type
@@ -38,8 +37,8 @@ describe(runInputMethodAsync, () => {
});
function mockDeviceData(udid: string, name: string, deviceClass: AppleDeviceClass): void {
asMock(prompts).mockImplementationOnce(() => ({ udid }));
asMock(prompts).mockImplementationOnce(() => ({ name }));
asMock(prompts).mockImplementationOnce(() => ({ deviceClass }));
asMock(prompts).mockImplementationOnce(() => ({ value: true }));
jest.mocked(prompts).mockImplementationOnce(async () => ({ udid }));
jest.mocked(prompts).mockImplementationOnce(async () => ({ name }));
jest.mocked(prompts).mockImplementationOnce(async () => ({ deviceClass }));
jest.mocked(prompts).mockImplementationOnce(async () => ({ value: true }));
}
+74 -22
View File
@@ -94,7 +94,7 @@ export type Account = {
apps: Array<App>;
/** @deprecated Build packs are no longer supported */
availableBuilds?: Maybe<Scalars['Int']>;
/** Billing information */
/** Billing information. Only visible to members with the ADMIN or OWNER role. */
billing?: Maybe<Billing>;
billingPeriod: BillingPeriod;
/** Build Jobs associated with this account */
@@ -338,7 +338,9 @@ export type AccountMutationRenameArgs = {
export type AccountMutationRequestRefundArgs = {
accountID: Scalars['ID'];
chargeIdentifier: Scalars['ID'];
chargeID: Scalars['ID'];
description?: InputMaybe<Scalars['String']>;
reason?: InputMaybe<Scalars['String']>;
};
@@ -393,21 +395,40 @@ export type AccountQueryByNameArgs = {
export type AccountUsageMetric = {
__typename?: 'AccountUsageMetric';
metric: EasServiceMetric;
id: Scalars['ID'];
metricType: UsageMetricType;
serviceMetric: EasServiceMetric;
timestamp: Scalars['DateTime'];
value: Scalars['Float'];
};
export type AccountUsageMetricAndCost = {
__typename?: 'AccountUsageMetricAndCost';
id: Scalars['ID'];
/** The limit, in units, allowed by this plan */
limit: Scalars['Float'];
metricType: UsageMetricType;
serviceMetric: EasServiceMetric;
/** Total cost of this particular metric, in cents */
totalCost: Scalars['Float'];
value: Scalars['Float'];
};
export type AccountUsageMetrics = {
__typename?: 'AccountUsageMetrics';
byBillingPeriod: UsageMetricTotal;
metricsForServiceMetric: Array<AccountUsageMetric>;
};
export type AccountUsageMetricsByBillingPeriodArgs = {
date: Scalars['DateTime'];
};
export type AccountUsageMetricsMetricsForServiceMetricArgs = {
granularity: UsageMetricsGranularity;
metric: EasServiceMetric;
serviceMetric: EasServiceMetric;
timespan: UsageMetricsTimespan;
};
@@ -1002,15 +1023,20 @@ export type AppDataInput = {
export type AppIcon = {
__typename?: 'AppIcon';
/** Nullable color palette of the app icon. If null, color palette couldn't be retrieved from external service (imgix) */
/** @deprecated No longer supported */
colorPalette?: Maybe<Scalars['JSON']>;
originalUrl: Scalars['String'];
primaryColor?: Maybe<Scalars['String']>;
url: Scalars['String'];
};
export type AppInfoInput = {
displayName?: InputMaybe<Scalars['String']>;
};
export type AppInput = {
accountId: Scalars['ID'];
appInfo?: InputMaybe<AppInfoInput>;
privacy: AppPrivacy;
projectName: Scalars['String'];
};
@@ -1021,6 +1047,8 @@ export type AppMutation = {
createApp: App;
/** @deprecated No longer supported */
grantAccess?: Maybe<App>;
/** Set display info for app */
setAppInfo: App;
/** Require api token to send push notifs for experience */
setPushSecurityEnabled: App;
};
@@ -1037,6 +1065,12 @@ export type AppMutationGrantAccessArgs = {
};
export type AppMutationSetAppInfoArgs = {
appId: Scalars['ID'];
appInfo: AppInfoInput;
};
export type AppMutationSetPushSecurityEnabledArgs = {
appId: Scalars['ID'];
pushSecurityEnabled: Scalars['Boolean'];
@@ -1905,6 +1939,13 @@ export type CodeSigningInfoInput = {
sig: Scalars['String'];
};
export type Concurrencies = {
__typename?: 'Concurrencies';
android: Scalars['Int'];
ios: Scalars['Int'];
total: Scalars['Int'];
};
export type CreateAccessTokenInput = {
actorID: Scalars['ID'];
note?: InputMaybe<Scalars['String']>;
@@ -1920,6 +1961,7 @@ export type CreateAccessTokenResponse = {
export type CreateAndroidSubmissionInput = {
appId: Scalars['ID'];
archiveUrl?: InputMaybe<Scalars['String']>;
config: AndroidSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
};
@@ -1937,17 +1979,11 @@ export type CreateEnvironmentSecretInput = {
export type CreateIosSubmissionInput = {
appId: Scalars['ID'];
archiveUrl?: InputMaybe<Scalars['String']>;
config: IosSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
};
export type CreateSubmissionInput = {
appId: Scalars['ID'];
config: Scalars['JSONObject'];
platform: AppPlatform;
submittedBuildId?: InputMaybe<Scalars['ID']>;
};
export type CreateSubmissionResult = {
__typename?: 'CreateSubmissionResult';
/** Created submission */
@@ -2057,7 +2093,10 @@ export enum EasBuildDeprecationInfoType {
}
export enum EasServiceMetric {
ManifestRequests = 'MANIFEST_REQUESTS'
AssetsRequests = 'ASSETS_REQUESTS',
BandwidthUsage = 'BANDWIDTH_USAGE',
ManifestRequests = 'MANIFEST_REQUESTS',
UniqueUsers = 'UNIQUE_USERS'
}
export type EditUpdateBranchInput = {
@@ -3108,6 +3147,8 @@ export type Submission = ActivityTimelineProjectActivity & {
actor?: Maybe<Actor>;
androidConfig?: Maybe<AndroidSubmissionConfig>;
app: App;
archiveUrl?: Maybe<Scalars['String']>;
canRetry: Scalars['Boolean'];
cancelingActor?: Maybe<Actor>;
createdAt: Scalars['DateTime'];
error?: Maybe<SubmissionError>;
@@ -3115,6 +3156,7 @@ export type Submission = ActivityTimelineProjectActivity & {
initiatingActor?: Maybe<Actor>;
iosConfig?: Maybe<IosSubmissionConfig>;
logsUrl?: Maybe<Scalars['String']>;
parentSubmission?: Maybe<Submission>;
platform: AppPlatform;
status: SubmissionStatus;
submittedBuild?: Maybe<Build>;
@@ -3159,16 +3201,13 @@ export type SubmissionMutation = {
createAndroidSubmission: CreateSubmissionResult;
/** Create an iOS EAS Submit submission */
createIosSubmission: CreateSubmissionResult;
/**
* Create an EAS Submit submission
* @deprecated Use createIosSubmission / createAndroidSubmission instead
*/
createSubmission: CreateSubmissionResult;
/** Retry an EAS Submit submission */
retrySubmission: CreateSubmissionResult;
};
export type SubmissionMutationCancelSubmissionArgs = {
submissionId?: InputMaybe<Scalars['ID']>;
submissionId: Scalars['ID'];
};
@@ -3182,8 +3221,8 @@ export type SubmissionMutationCreateIosSubmissionArgs = {
};
export type SubmissionMutationCreateSubmissionArgs = {
input: CreateSubmissionInput;
export type SubmissionMutationRetrySubmissionArgs = {
parentSubmissionId: Scalars['ID'];
};
export type SubmissionQuery = {
@@ -3210,6 +3249,7 @@ export type SubscriptionDetails = {
__typename?: 'SubscriptionDetails';
addons: Array<AddonDetails>;
cancelledAt?: Maybe<Scalars['DateTime']>;
concurrencies?: Maybe<Concurrencies>;
endedAt?: Maybe<Scalars['DateTime']>;
id: Scalars['ID'];
isDowngrading?: Maybe<Scalars['Boolean']>;
@@ -3395,8 +3435,20 @@ export enum UploadSessionType {
EasSubmitAppArchive = 'EAS_SUBMIT_APP_ARCHIVE'
}
export type UsageMetricTotal = {
__typename?: 'UsageMetricTotal';
billingPeriod: BillingPeriod;
id: Scalars['ID'];
overageMetrics: Array<AccountUsageMetricAndCost>;
planMetrics: Array<AccountUsageMetricAndCost>;
/** Total cost of overages, in cents */
totalCost: Scalars['Float'];
};
export enum UsageMetricType {
Request = 'REQUEST'
Bandwidth = 'BANDWIDTH',
Request = 'REQUEST',
User = 'USER'
}
export enum UsageMetricsGranularity {
@@ -0,0 +1,23 @@
import { AgeRatingDeclaration, KidsAge } from '@expo/apple-utils';
import { AttributesOf } from '../../../../utils/asc';
export const kidsSixToEightAdvisory: AttributesOf<AgeRatingDeclaration> = {
alcoholTobaccoOrDrugUseOrReferences: null,
gamblingSimulated: null,
gambling: null,
contests: null,
medicalOrTreatmentInformation: null,
profanityOrCrudeHumor: null,
sexualContentGraphicAndNudity: null,
sexualContentOrNudity: null,
horrorOrFearThemes: null,
matureOrSuggestiveThemes: null,
violenceCartoonOrFantasy: null,
violenceRealisticProlongedGraphicOrSadistic: null,
violenceRealistic: null,
unrestrictedWebAccess: false,
seventeenPlus: false,
kidsAgeBand: KidsAge.SIX_TO_EIGHT,
gamblingAndContests: false,
};
@@ -0,0 +1,23 @@
import { AppCategory, AppInfo, BundleIdPlatform } from '@expo/apple-utils';
import { AttributesOf } from '../../../../utils/asc';
export const primaryOnlyCategory: Pick<AttributesOf<AppInfo>, 'primaryCategory'> = {
primaryCategory: new AppCategory({} as any, 'ENTERTAINMENT', {
platforms: [BundleIdPlatform.IOS, BundleIdPlatform.MAC_OS],
}),
};
export const secondaryOnlyCategory: Pick<AttributesOf<AppInfo>, 'secondaryCategory'> = {
secondaryCategory: new AppCategory({} as any, 'GAMES', {
platforms: [BundleIdPlatform.IOS, BundleIdPlatform.MAC_OS],
}),
};
export const primaryAndSecondaryCategory: Pick<
AttributesOf<AppInfo>,
'primaryCategory' | 'secondaryCategory'
> = {
...primaryOnlyCategory,
...secondaryOnlyCategory,
};
@@ -0,0 +1,21 @@
import { AppInfoLocalization } from '@expo/apple-utils';
import { AttributesOf } from '../../../../utils/asc';
export const englishInfo: AttributesOf<AppInfoLocalization> = {
locale: 'en-US',
name: 'Awesome test app',
subtitle: 'This is just a test',
privacyPolicyUrl: 'https://example.com/en/privacy-policy',
privacyChoicesUrl: 'https://exmaple.com/en/privacy-choices',
privacyPolicyText: 'This is some privacy policy text',
};
export const dutchInfo: AttributesOf<AppInfoLocalization> = {
locale: 'nl-NL',
name: 'Geweldige test app',
subtitle: 'Dit is maar een test',
privacyPolicyUrl: 'https://example.com/nl/privacy-policy',
privacyChoicesUrl: 'https://exmaple.com/nl/privacy-choices',
privacyPolicyText: 'Dit is wat privacy policy tekst',
};
@@ -0,0 +1,48 @@
import { AppStoreState, AppStoreVersion, Platform, ReleaseType } from '@expo/apple-utils';
import { AttributesOf } from '../../../../utils/asc';
export const manualRelease: AttributesOf<AppStoreVersion> = {
platform: Platform.IOS,
versionString: '1.0.0',
appStoreState: AppStoreState.WAITING_FOR_REVIEW,
storeIcon: null,
watchStoreIcon: null,
copyright: '2022 - ACME',
releaseType: ReleaseType.MANUAL,
earliestReleaseDate: null,
usesIdfa: null,
isWatchOnly: false,
downloadable: false,
createdDate: '2022-05-23T00:00:00.000Z',
};
export const automaticRelease: AttributesOf<AppStoreVersion> = {
platform: Platform.IOS,
versionString: '2.0.0',
appStoreState: AppStoreState.WAITING_FOR_REVIEW,
storeIcon: null,
watchStoreIcon: null,
copyright: '2022 - ACME',
releaseType: ReleaseType.AFTER_APPROVAL,
earliestReleaseDate: null,
usesIdfa: null,
isWatchOnly: false,
downloadable: false,
createdDate: '2022-05-23T00:00:00.000Z',
};
export const scheduledRelease: AttributesOf<AppStoreVersion> = {
platform: Platform.IOS,
versionString: '3.0.0',
appStoreState: AppStoreState.READY_FOR_SALE,
storeIcon: null,
watchStoreIcon: null,
copyright: '2022 - ACME',
releaseType: ReleaseType.SCHEDULED,
earliestReleaseDate: '2022-05-29T00:00:00.000Z',
usesIdfa: null,
isWatchOnly: false,
downloadable: false,
createdDate: '2022-05-23T00:00:00.000Z',
};
@@ -0,0 +1,23 @@
import { AppStoreVersionLocalization } from '@expo/apple-utils';
import { AttributesOf } from '../../../../utils/asc';
export const englishVersion: AttributesOf<AppStoreVersionLocalization> = {
locale: 'en-US',
description: 'This is a description of this version',
keywords: 'some, description',
marketingUrl: 'https://example.com/en/marketing',
promotionalText: 'This is some promotional text',
supportUrl: 'https://example.com/en/support',
whatsNew: 'Bugfixes and improvements',
};
export const dutchVersion: AttributesOf<AppStoreVersionLocalization> = {
locale: 'nl-NL',
description: 'Dit is een beschrijving van deze versie',
keywords: 'een, beschrijving',
marketingUrl: 'https://example.com/nl/marketing',
promotionalText: 'Dit is wat promotie tekst',
supportUrl: 'https://example.com/nl/support',
whatsNew: 'Beestreparaties en verbeteringen',
};
@@ -0,0 +1,151 @@
import { AppleConfigWriter } from '../writer';
import { kidsSixToEightAdvisory } from './fixtures/ageRatingDeclaration';
import { primaryAndSecondaryCategory, secondaryOnlyCategory } from './fixtures/appInfo';
import { dutchInfo, englishInfo } from './fixtures/appInfoLocalization';
import { automaticRelease, manualRelease, scheduledRelease } from './fixtures/appStoreVersion';
import { dutchVersion, englishVersion } from './fixtures/appStoreVersionLocalization';
describe('toSchema', () => {
it('returns object with apple schema', () => {
const writer = new AppleConfigWriter();
expect(writer.toSchema()).toMatchObject({
configVersion: 0,
apple: expect.any(Object),
});
});
});
describe('setAgeRating', () => {
it('modifies the advisory', () => {
const writer = new AppleConfigWriter();
writer.setAgeRating(kidsSixToEightAdvisory);
expect(writer.schema.advisory).toMatchObject(kidsSixToEightAdvisory);
});
});
describe('setInfoLocale', () => {
it('creates and modifies the locale', () => {
const writer = new AppleConfigWriter();
writer.setInfoLocale(englishInfo);
expect(writer.schema.info?.[englishInfo.locale]).toMatchObject({
title: englishInfo.name,
subtitle: englishInfo.subtitle,
privacyPolicyUrl: englishInfo.privacyPolicyUrl,
privacyPolicyText: englishInfo.privacyPolicyText,
privacyChoicesUrl: englishInfo.privacyChoicesUrl,
});
});
it('modifies existing locales', () => {
const writer = new AppleConfigWriter();
writer.setInfoLocale(englishInfo);
writer.setInfoLocale(dutchInfo);
writer.setInfoLocale({
...englishInfo,
name: 'This is now different',
privacyPolicyText: null,
});
expect(writer.schema.info?.[dutchInfo.locale]).toHaveProperty('title', dutchInfo.name);
expect(writer.schema.info?.[englishInfo.locale]).toMatchObject({
title: 'This is now different',
privacyPolicyText: undefined,
});
});
});
describe('setCategories', () => {
it('modifies the categories', () => {
const writer = new AppleConfigWriter();
writer.setCategories(primaryAndSecondaryCategory as any);
expect(writer.schema.categories).toHaveLength(2);
expect((writer.schema.categories as any)[0]).toBe(
primaryAndSecondaryCategory.primaryCategory?.id
);
expect((writer.schema.categories as any)[1]).toBe(
primaryAndSecondaryCategory.secondaryCategory?.id
);
});
it('skips secondary category without primary category', () => {
const writer = new AppleConfigWriter();
writer.setCategories(secondaryOnlyCategory as any);
expect(writer.schema.categories).toHaveLength(0);
});
});
describe('setVersion', () => {
it('modifies the copyright', () => {
const writer = new AppleConfigWriter();
writer.setVersion(manualRelease);
expect(writer.schema.copyright).toBe(manualRelease.copyright);
});
});
describe('setVersionRelease', () => {
it('modifies scheduled release', () => {
const writer = new AppleConfigWriter();
writer.setVersionRelease(scheduledRelease);
expect(writer.schema.release).toMatchObject({
autoReleaseDate: scheduledRelease.earliestReleaseDate,
});
});
it('modifies automatic release', () => {
const writer = new AppleConfigWriter();
writer.setVersionRelease(automaticRelease);
expect(writer.schema.release).toMatchObject({
automaticRelease: true,
});
});
it('modifies manual release', () => {
const writer = new AppleConfigWriter();
writer.setVersionRelease(manualRelease);
expect(writer.schema.release).toMatchObject({
automaticRelease: false,
});
});
it('overwrites all release fields', () => {
const writer = new AppleConfigWriter();
writer.setVersionRelease(scheduledRelease);
writer.setVersionRelease(manualRelease);
expect(writer.schema.release).not.toHaveProperty('autoReleaseDate');
});
});
describe('setVersionLocale', () => {
it('creates and modifies the locale', () => {
const writer = new AppleConfigWriter();
writer.setVersionLocale(englishVersion);
expect(writer.schema.info?.[englishVersion.locale]).toMatchObject({
description: englishVersion.description,
keywords: englishVersion.keywords?.split(', '),
releaseNotes: englishVersion.whatsNew,
marketingUrl: englishVersion.marketingUrl,
promoText: englishVersion.promotionalText,
supportUrl: englishVersion.supportUrl,
});
});
it('modifies existing locales', () => {
const writer = new AppleConfigWriter();
writer.setVersionLocale(englishVersion);
writer.setVersionLocale(dutchVersion);
writer.setVersionLocale({
...englishVersion,
description: 'This is now different',
whatsNew: null,
});
expect(writer.schema.info?.[dutchVersion.locale]).toHaveProperty(
'description',
dutchVersion.description
);
expect(writer.schema.info?.[englishVersion.locale]).toMatchObject({
description: 'This is now different',
releaseNotes: undefined,
});
});
});
@@ -0,0 +1,121 @@
import {
AgeRatingDeclaration,
AppInfoLocalization,
AppStoreVersion,
AppStoreVersionLocalization,
CategoryIds,
ReleaseType,
} from '@expo/apple-utils';
import { unique } from '../../utils/array';
import { AttributesOf } from '../../utils/asc';
import { removeDatePrecision } from '../../utils/date';
import { AppleMetadata } from '../types';
type PartialExcept<T, K extends keyof T> = Pick<T, K> & Partial<Omit<T, K>>;
// TODO: find out if we can move this to default JSON schema normalization
export const DEFAULT_WHATSNEW = 'Bug fixes and improved stability';
/**
* Deserializes the metadata config schema into attributes for different models.
* This uses version 0 of the config schema.
*/
export class AppleConfigReader {
constructor(public readonly schema: AppleMetadata) {}
getAgeRating(): Partial<AttributesOf<AgeRatingDeclaration>> | null {
return this.schema.advisory || null;
}
getLocales(): string[] {
// TODO: filter "default" locales, add option to add non-localized info to the config
return unique(Object.keys(this.schema.info || {}));
}
getInfoLocale(
locale: string
): PartialExcept<AttributesOf<AppInfoLocalization>, 'locale' | 'name'> | null {
const info = this.schema.info?.[locale];
if (!info) {
return null;
}
return {
locale,
name: info.title || 'no name provided',
subtitle: info.subtitle,
privacyChoicesUrl: info.privacyChoicesUrl,
privacyPolicyText: info.privacyPolicyText,
privacyPolicyUrl: info.privacyPolicyUrl,
};
}
getCategories(): CategoryIds | null {
if (Array.isArray(this.schema.categories) && this.schema.categories.length > 0) {
return {
primaryCategory: this.schema.categories[0],
secondaryCategory: this.schema.categories[1],
};
}
return null;
}
getVersion(): Partial<
Omit<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
> | null {
return this.schema.copyright ? { copyright: this.schema.copyright } : null;
}
getVersionRelease(): Partial<
Pick<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
> | null {
const { release } = this.schema;
if (release?.autoReleaseDate) {
return {
releaseType: ReleaseType.SCHEDULED,
// Convert time format to 2020-06-17T12:00:00-07:00
earliestReleaseDate: removeDatePrecision(release.autoReleaseDate)?.toISOString() ?? null,
};
}
if (release?.automaticRelease === true) {
return {
releaseType: ReleaseType.AFTER_APPROVAL,
earliestReleaseDate: null,
};
}
if (release?.automaticRelease === false) {
return {
releaseType: ReleaseType.MANUAL,
earliestReleaseDate: null,
};
}
return null;
}
getVersionLocale(
locale: string,
context: { versionIsFirst: boolean }
): Partial<AttributesOf<AppStoreVersionLocalization>> | null {
const info = this.schema.info?.[locale];
if (!info) {
return null;
}
return {
locale,
description: info.description,
keywords: info.keywords?.join(', '),
// TODO: maybe move this to task logic, it's more an exception than data handling
whatsNew: context.versionIsFirst ? undefined : info.releaseNotes || DEFAULT_WHATSNEW,
marketingUrl: info.marketingUrl,
promotionalText: info.promoText,
supportUrl: info.supportUrl,
};
}
}
@@ -0,0 +1,107 @@
import {
AgeRatingDeclaration,
AppInfo,
AppInfoLocalization,
AppStoreVersion,
AppStoreVersionLocalization,
ReleaseType,
} from '@expo/apple-utils';
import { AttributesOf } from '../../utils/asc';
import { AppleMetadata } from '../types';
/**
* Serializes the Apple ASC entities into the metadata configuration schema.
* This uses version 0 of the config schema.
*/
export class AppleConfigWriter {
constructor(public readonly schema: Partial<AppleMetadata> = {}) {}
/** Get the schema result to write it to the config file */
toSchema(): { configVersion: number; apple: Partial<AppleMetadata> } {
return {
configVersion: 0,
apple: this.schema,
};
}
setAgeRating(attributes: AttributesOf<AgeRatingDeclaration>): void {
this.schema.advisory = attributes;
}
setInfoLocale(attributes: AttributesOf<AppInfoLocalization>): void {
this.schema.info = this.schema.info ?? {};
const existing = this.schema.info[attributes.locale] ?? {};
this.schema.info[attributes.locale] = {
...existing,
title: attributes.name ?? 'no name provided',
subtitle: optional(attributes.subtitle),
privacyPolicyUrl: optional(attributes.privacyPolicyUrl),
privacyPolicyText: optional(attributes.privacyPolicyText),
privacyChoicesUrl: optional(attributes.privacyChoicesUrl),
};
}
setCategories({ primaryCategory, secondaryCategory }: AttributesOf<AppInfo>): void {
this.schema.categories = [];
// TODO: see why these types are conflicting
if (primaryCategory) {
this.schema.categories.push(primaryCategory.id as any);
if (secondaryCategory) {
this.schema.categories.push(secondaryCategory.id as any);
}
}
}
setVersion(
attributes: Omit<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
): void {
this.schema.copyright = optional(attributes.copyright);
}
setVersionRelease(
attributes: Pick<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
): void {
if (attributes.releaseType === ReleaseType.SCHEDULED) {
this.schema.release = {
autoReleaseDate: optional(attributes.earliestReleaseDate),
};
}
if (attributes.releaseType === ReleaseType.AFTER_APPROVAL) {
this.schema.release = {
automaticRelease: true,
};
}
if (attributes.releaseType === ReleaseType.MANUAL) {
this.schema.release = {
automaticRelease: false,
};
}
}
setVersionLocale(attributes: AttributesOf<AppStoreVersionLocalization>): void {
this.schema.info = this.schema.info ?? {};
const existing = this.schema.info[attributes.locale] ?? {};
this.schema.info[attributes.locale] = {
...existing,
description: optional(attributes.description),
keywords: optional(attributes.keywords)
?.split(',')
.map(keyword => keyword.trim()),
releaseNotes: optional(attributes.whatsNew),
marketingUrl: optional(attributes.marketingUrl),
promoText: optional(attributes.promotionalText),
supportUrl: optional(attributes.supportUrl),
};
}
}
/** Helper function to convert `T | null` to `T | undefined` */
function optional<T>(value: T | null): T | undefined {
return value ?? undefined;
}
@@ -0,0 +1,18 @@
import type { App } from '@expo/apple-utils';
import type { AgeRatingData } from './tasks/age-rating';
import type { AppInfoData } from './tasks/app-info';
import type { AppVersionData } from './tasks/app-version';
/**
* 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;
/**
* 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'>>;
@@ -0,0 +1,31 @@
import { AppleConfigReader } from './config/reader';
import { AppleConfigWriter } from './config/writer';
import { AppleData, PartialAppleData } from './data';
export abstract class AppleTask {
/** Get a description from the task to use as section headings in the log */
abstract name(): string;
/** Prepare the data from the App Store to start syncing with the store configuration */
abstract prepareAsync(options: TaskPrepareOptions): Promise<void>;
/** Download all information from the App Store to generate the store configuration */
abstract downloadAsync(options: TaskDownloadOptions): Promise<void>;
/** Upload all information from the store configuration to the App Store */
abstract uploadAsync(options: TaskUploadOptions): Promise<void>;
}
export type TaskPrepareOptions = {
context: PartialAppleData;
};
export type TaskDownloadOptions = {
config: AppleConfigWriter;
context: AppleData;
};
export type TaskUploadOptions = {
config: AppleConfigReader;
context: AppleData;
};
@@ -0,0 +1,8 @@
import { getRequestClient } from '@expo/apple-utils';
export { default } from 'nock';
// Axios from apple-utils needs to be patched when we use Nock.
// Instead of the default adapter, we need to use `axios/lib/adapters/http`
// see: https://github.com/nock/nock#axios
getRequestClient().defaults.adapter = require('axios/lib/adapters/http');
@@ -0,0 +1,84 @@
import { AgeRatingDeclaration, AppStoreVersion, Rating } from '@expo/apple-utils';
import nock from 'nock';
import { AppleConfigReader } from '../../config/reader';
import { AppleData } from '../../data';
import { AgeRatingTask } from '../age-rating';
import { requestContext } from './fixtures/requestContext';
jest.mock('../../../../ora');
describe(AgeRatingTask, () => {
describe('preuploadAsync', () => {
it('aborts when version is not loaded', async () => {
const promise = new AgeRatingTask().prepareAsync({ context: {} as any });
await expect(promise).rejects.toThrow('not prepared');
});
it('loads age rating from app version', async () => {
const scope = nock('https://api.appstoreconnect.apple.com')
.get(`/v1/${AppStoreVersion.type}/stub-id/ageRatingDeclaration`)
.reply(200, require('./fixtures/appStoreVersions/get-ageRatingDeclaration-200.json'));
const context: any = {
version: new AppStoreVersion(requestContext, 'stub-id', {} as any),
};
await new AgeRatingTask().prepareAsync({ context });
expect(context.ageRating).toBeInstanceOf(AgeRatingDeclaration);
expect(scope.isDone()).toBeTruthy();
});
});
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 updating age rating when not configured', async () => {
const scope = nock('https://api.appstoreconnect.apple.com')
.patch(`/v1/${AgeRatingDeclaration.type}/stub-id`)
.reply(200, require('./fixtures/ageRatingDeclarations/patch-200.json'));
await new AgeRatingTask().uploadAsync({
config: new AppleConfigReader({ advisory: undefined }),
context: {
ageRating: new AgeRatingDeclaration(requestContext, 'stub-id', {} as any),
} as AppleData,
});
expect(scope.isDone()).toBeFalsy();
nock.cleanAll();
});
it('updates age rating from config when configured', async () => {
const scope = nock('https://api.appstoreconnect.apple.com')
.patch(`/v1/${AgeRatingDeclaration.type}/stub-id`)
.reply(200, require('./fixtures/ageRatingDeclarations/patch-200.json'));
const context = {
ageRating: new AgeRatingDeclaration(requestContext, 'stub-id', {} as any),
};
await new AgeRatingTask().uploadAsync({
config: new AppleConfigReader({
advisory: {
// See fixture value for horrorOrFearThemes
horrorOrFearThemes: Rating.INFREQUENT_OR_MILD,
},
}),
context: context as AppleData,
});
expect(context.ageRating.id).not.toMatch('stub-id');
expect(scope.isDone()).toBeTruthy();
});
});
});
@@ -0,0 +1,114 @@
import { App, AppCategoryId, AppInfo, AppInfoLocalization } from '@expo/apple-utils';
import nock from 'nock';
import { AppleConfigReader } from '../../config/reader';
import { AppleData, PartialAppleData } from '../../data';
import { AppInfoTask } from '../app-info';
import { requestContext } from './fixtures/requestContext';
jest.mock('../../../../ora');
describe(AppInfoTask, () => {
describe('preuploadAsync', () => {
it('loads editable app info and locales from app instance', async () => {
const scopeInfo = nock('https://api.appstoreconnect.apple.com')
.get(uri => uri.startsWith(`/v1/${App.type}/stub-id/${AppInfo.type}`)) // allow any query params
.reply(200, require('./fixtures/apps/get-appInfos-200.json'));
const scopeLocales = nock('https://api.appstoreconnect.apple.com')
.get(/\/v1\/appInfos\/.*\/appInfoLocalizations/) // allow any id from fixture
.reply(200, require('./fixtures/appInfos/get-appInfoLocalizations-200.json'));
const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
};
await new AppInfoTask().prepareAsync({ context });
expect(context.info).toBeInstanceOf(AppInfo);
expect(context.infoLocales).toBeInstanceOf(Array);
expect(scopeInfo.isDone()).toBeTruthy();
expect(scopeLocales.isDone()).toBeTruthy();
});
});
describe('uploadAsync', () => {
it('aborts when app info is not loaded', async () => {
const promise = new AppInfoTask().uploadAsync({
config: new AppleConfigReader({}),
context: { info: undefined } as any,
});
await expect(promise).rejects.toThrow('info not initialized');
});
it('skips updating categories when not configured', async () => {
const scope = nock('https://api.appstoreconnect.apple.com')
.patch(`/v1/${AppInfo.type}/stub-id`)
.reply(200, require('./fixtures/appInfos/patch-200.json'));
await new AppInfoTask().uploadAsync({
config: new AppleConfigReader({ categories: undefined }),
context: {
info: new AppInfo(requestContext, 'stub-id', {} as any),
} as AppleData,
});
expect(scope.isDone()).toBeFalsy();
nock.cleanAll();
});
it('skips updating localized info when not configured', async () => {
const scope = nock('https://api.appstoreconnect.apple.com')
.patch(`/v1/${AppInfoLocalization.type}/stub-id`)
.reply(200, require('./fixtures/appInfos/patch-200.json'));
await new AppInfoTask().uploadAsync({
config: new AppleConfigReader({ info: undefined }),
context: {
info: new AppInfo(requestContext, 'stub-id', {} as any),
} as AppleData,
});
expect(scope.isDone()).toBeFalsy();
nock.cleanAll();
});
it('updates categories and localized info', async () => {
const updateCategoryScope = nock('https://api.appstoreconnect.apple.com')
.patch(`/v1/${AppInfo.type}/stub-id`)
.reply(200, require('./fixtures/appInfos/patch-200.json'));
const updateENInfoScope = nock('https://api.appstoreconnect.apple.com')
.patch(`/v1/${AppInfoLocalization.type}/APP_INFO_LOCALE_1`) // see fixture ID
.reply(200, require('./fixtures/appInfoLocalizations/patch-200.json'));
const updateNLInfoScope = nock('https://api.appstoreconnect.apple.com')
.patch(`/v1/${AppInfoLocalization.type}/APP_INFO_LOCALE_2`) // see fixture ID
.reply(200, require('./fixtures/appInfoLocalizations/patch-200.json'));
const fetchLocalizedInfoScope = nock('https://api.appstoreconnect.apple.com')
.get(`/v1/${AppInfo.type}/APP_INFO_1/${AppInfoLocalization.type}`) // see fixture ID
.twice()
.reply(200, require('./fixtures/appInfos/get-appInfoLocalizations-200.json'));
await new AppInfoTask().uploadAsync({
config: new AppleConfigReader({
categories: [AppCategoryId.ENTERTAINMENT],
info: {
'en-US': { title: 'Hello' },
'nl-NL': { title: 'Hallo' },
},
}),
context: {
info: new AppInfo(requestContext, 'stub-id', {} as any),
} as AppleData,
});
expect(updateCategoryScope.isDone()).toBeTruthy();
expect(fetchLocalizedInfoScope.isDone()).toBeTruthy();
expect(updateENInfoScope.isDone()).toBeTruthy();
expect(updateNLInfoScope.isDone()).toBeTruthy();
});
});
});
@@ -0,0 +1,176 @@
import {
App,
AppStoreState,
AppStoreVersion,
AppStoreVersionLocalization,
} from '@expo/apple-utils';
import nock from 'nock';
import { AppleConfigReader } from '../../config/reader';
import { AppleData, PartialAppleData } from '../../data';
import { AppVersionTask } from '../app-version';
import { requestContext } from './fixtures/requestContext';
jest.mock('../../../../ora');
describe(AppVersionTask, () => {
describe('preuploadAsync', () => {
it('loads live version', async () => {
const scope = nock('https://api.appstoreconnect.apple.com')
// Respond to app.getLiveAppStoreVersionAsync
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query((params: any) => params['filter[appStoreState]'] === AppStoreState.READY_FOR_SALE)
.reply(200, require('./fixtures/apps/get-appStoreVersions-200.json'))
// Respond to app.getAppStoreVersionsAsync
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query((params: any) => params['filter[appStoreState]'] !== AppStoreState.READY_FOR_SALE)
.reply(200, require('./fixtures/apps/get-appStoreVersions-count-200.json'))
// Respond to version.getLocalizationsAsync
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/${AppStoreVersionLocalization.type}`)
.reply(
200,
require('./fixtures/appStoreVersions/get-appStoreVersionLocalizations-200.json')
);
const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
};
await new AppVersionTask({ editLive: true }).prepareAsync({ context });
expect(context.version).toBeInstanceOf(AppStoreVersion);
expect(context.versionIsLive).toBeTruthy();
expect(context.versionIsFirst).toBeTruthy();
expect(scope.isDone()).toBeTruthy();
});
it('falls back to editable version if live version is not found', async () => {
const scope = nock('https://api.appstoreconnect.apple.com')
// Respond to app.getLiveAppStoreVersionAsync, failing with 404
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query((params: any) => params['filter[appStoreState]'] === AppStoreState.READY_FOR_SALE)
.reply(404, {})
// Respond to app.getEditAppStoreVersionAsync
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query((params: any) => params['filter[appStoreState]'] !== AppStoreState.READY_FOR_SALE)
.reply(200, require('./fixtures/apps/get-appStoreVersions-count-200.json'))
// Respond to app.getAppStoreVersionsAsync
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query(true)
.reply(200, require('./fixtures/apps/get-appStoreVersions-count-200.json'))
// Respond to version.getLocalizationsAsync
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/${AppStoreVersionLocalization.type}`)
.reply(
200,
require('./fixtures/appStoreVersions/get-appStoreVersionLocalizations-200.json')
);
const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
};
await new AppVersionTask({ editLive: true }).prepareAsync({ context });
expect(context.version).toBeInstanceOf(AppStoreVersion);
expect(context.versionIsLive).toBeFalsy();
expect(context.versionIsFirst).toBeTruthy();
expect(scope.isDone()).toBeTruthy();
});
it('loads last editable version', async () => {
const scope = nock('https://api.appstoreconnect.apple.com')
// Respond to app.getEditAppStoreVersionAsync
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query((params: any) => params['filter[appStoreState]'] !== AppStoreState.READY_FOR_SALE)
.reply(200, require('./fixtures/apps/get-appStoreVersions-200.json'))
// Respond to app.getAppStoreVersionsAsync
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query((params: any) => params['filter[appStoreState]'] !== AppStoreState.READY_FOR_SALE)
.reply(200, require('./fixtures/apps/get-appStoreVersions-count-200.json'))
// Respond to version.getLocalizationsAsync
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/${AppStoreVersionLocalization.type}`)
.reply(
200,
require('./fixtures/appStoreVersions/get-appStoreVersionLocalizations-200.json')
);
const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
};
await new AppVersionTask().prepareAsync({ context });
expect(context.version).toBeInstanceOf(AppStoreVersion);
expect(context.versionIsLive).toBeFalsy();
expect(context.versionIsFirst).toBeTruthy();
expect(scope.isDone()).toBeTruthy();
});
it('sets first version property for multiple versions', async () => {
const scope = nock('https://api.appstoreconnect.apple.com')
// Respond to app.getEditAppStoreVersionAsync
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query((params: any) => params['filter[appStoreState]'] !== AppStoreState.READY_FOR_SALE)
.reply(200, require('./fixtures/apps/get-appStoreVersions-200.json'))
// Respond to app.getAppStoreVersionsAsync
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query((params: any) => params['filter[appStoreState]'] !== AppStoreState.READY_FOR_SALE)
.reply(200, require('./fixtures/apps/get-appStoreVersions-count-multiple-200.json'))
// Respond to version.getLocalizationsAsync
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/${AppStoreVersionLocalization.type}`)
.reply(
200,
require('./fixtures/appStoreVersions/get-appStoreVersionLocalizations-200.json')
);
const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
};
await new AppVersionTask().prepareAsync({ context });
expect(context.versionIsFirst).toBeFalsy();
expect(scope.isDone()).toBeTruthy();
});
});
describe('upload', () => {
it('aborts when version is not loaded', async () => {
const promise = new AppVersionTask().uploadAsync({
context: {} as any,
config: new AppleConfigReader({}),
});
await expect(promise).rejects.toThrow('not initialized');
});
it('updates version and release from config', async () => {
const config = new AppleConfigReader({
copyright: '2022 ACME',
release: { automaticRelease: true },
});
const attributes = { ...config.getVersion(), ...config.getVersionRelease() };
const scope = nock('https://api.appstoreconnect.apple.com')
// Respond to version.updateAsync
.patch(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1`)
.reply(200, {
data: {
id: 'APP_STORE_VERSION_1',
type: AppStoreVersion.type,
attributes,
},
});
const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
};
await new AppVersionTask().uploadAsync({ context: context as AppleData, config });
expect(context.version?.attributes).toMatchObject(attributes);
expect(scope.isDone()).toBeTruthy();
});
});
});
@@ -0,0 +1,31 @@
{
"data": {
"type": "ageRatingDeclarations",
"id": "AGE_RATING_1",
"attributes": {
"alcoholTobaccoOrDrugUseOrReferences": "NONE",
"contests": "NONE",
"gamblingAndContests": false,
"gambling": false,
"gamblingSimulated": "NONE",
"kidsAgeBand": null,
"medicalOrTreatmentInformation": "NONE",
"profanityOrCrudeHumor": "NONE",
"sexualContentGraphicAndNudity": "NONE",
"sexualContentOrNudity": "NONE",
"seventeenPlus": false,
"horrorOrFearThemes": "INFREQUENT_OR_MILD",
"matureOrSuggestiveThemes": "NONE",
"unrestrictedWebAccess": false,
"violenceCartoonOrFantasy": "NONE",
"violenceRealisticProlongedGraphicOrSadistic": "NONE",
"violenceRealistic": "NONE"
},
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/ageRatingDeclarations/AGE_RATING_1"
}
},
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/ageRatingDeclarations/AGE_RATING_1"
}
}
@@ -0,0 +1,20 @@
{
"data": {
"type": "appInfoLocalizations",
"id": "APP_INFO_LOCALE_1",
"attributes": {
"locale": "en-US",
"name": "Rexpo Square App",
"subtitle": "144 fps red squares",
"privacyPolicyUrl": "https://github.com/bycedric/red-square-app",
"privacyChoicesUrl": null,
"privacyPolicyText": null
},
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appInfoLocalizations/APP_INFO_LOCALE_1"
}
},
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appInfoLocalizations/APP_INFO_LOCALE_1"
}
}
@@ -0,0 +1,43 @@
{
"data": [
{
"type": "appInfoLocalizations",
"id": "APP_INFO_LOCALE_1",
"attributes": {
"locale": "en-US",
"name": "Rexpo Square App",
"subtitle": "144 fps red squares",
"privacyPolicyUrl": "https://github.com/bycedric/red-square-app",
"privacyChoicesUrl": null,
"privacyPolicyText": null
},
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appInfoLocalizations/APP_INFO_LOCALE_1"
}
},
{
"type": "appInfoLocalizations",
"id": "APP_INFO_LOCALE_2",
"attributes": {
"locale": "nl-NL",
"name": "Rexpo Vierkant App",
"subtitle": "144 fps red vierkant",
"privacyPolicyUrl": "https://github.com/bycedric/red-square-app",
"privacyChoicesUrl": null,
"privacyPolicyText": null
},
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appInfoLocalizations/APP_INFO_LOCALE_2"
}
}
],
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/a9c2ad6a-182f-4021-9456-6b609ebc5eab/appInfoLocalizations"
},
"meta": {
"paging": {
"total": 2,
"limit": 50
}
}
}
@@ -0,0 +1,68 @@
{
"data": {
"type": "appInfos",
"id": "APP_INFO_1",
"attributes": {
"appStoreState": "PREPARE_FOR_SUBMISSION",
"appStoreAgeRating": "NINE_PLUS",
"brazilAgeRating": "TEN",
"kidsAgeBand": null
},
"relationships": {
"ageRatingDeclaration": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/ageRatingDeclaration",
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/ageRatingDeclaration"
}
},
"appInfoLocalizations": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/appInfoLocalizations",
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/appInfoLocalizations"
}
},
"primaryCategory": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/primaryCategory",
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/primaryCategory"
}
},
"primarySubcategoryOne": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/primarySubcategoryOne",
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/primarySubcategoryOne"
}
},
"primarySubcategoryTwo": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/primarySubcategoryTwo",
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/primarySubcategoryTwo"
}
},
"secondaryCategory": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/secondaryCategory",
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/secondaryCategory"
}
},
"secondarySubcategoryOne": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/secondarySubcategoryOne",
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/secondarySubcategoryOne"
}
},
"secondarySubcategoryTwo": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/secondarySubcategoryTwo",
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/secondarySubcategoryTwo"
}
}
},
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1"
}
},
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1"
}
}
@@ -0,0 +1,31 @@
{
"data": {
"type": "ageRatingDeclarations",
"id": "a9c2ad6a-182f-4021-9456-6b609ebc5eab",
"attributes": {
"alcoholTobaccoOrDrugUseOrReferences": "NONE",
"contests": "NONE",
"gamblingAndContests": false,
"gambling": false,
"gamblingSimulated": "NONE",
"kidsAgeBand": null,
"medicalOrTreatmentInformation": "NONE",
"profanityOrCrudeHumor": "NONE",
"sexualContentGraphicAndNudity": "NONE",
"sexualContentOrNudity": "NONE",
"seventeenPlus": false,
"horrorOrFearThemes": "NONE",
"matureOrSuggestiveThemes": "NONE",
"unrestrictedWebAccess": false,
"violenceCartoonOrFantasy": "NONE",
"violenceRealisticProlongedGraphicOrSadistic": "NONE",
"violenceRealistic": "NONE"
},
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/ageRatingDeclarations/a9c2ad6a-182f-4021-9456-6b609ebc5eab"
}
},
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/949768f6-58e5-46b4-b046-23861f61831a/ageRatingDeclaration"
}
}
@@ -0,0 +1,11 @@
{
"errors": [
{
"id": "9901eeb2-81e7-4562-9beb-c5f72010ab71",
"status": "404",
"code": "NOT_FOUND",
"title": "The specified resource does not exist",
"detail": "There is no resource of type 'appStoreVersions' with id 'non-existing'"
}
]
}
@@ -0,0 +1,70 @@
{
"data": [{
"type": "appStoreVersionLocalizations",
"id": "4bad54bc-3810-401a-a7f4-b02ef8206c41",
"attributes": {
"description": "Some of the best apps",
"locale": "nl-NL",
"keywords": "meme, red, square, x2",
"marketingUrl": "https://github.com/bycedric/red-square-app",
"promotionalText": null,
"supportUrl": "https://github.com/bycedric/red-square-app/issues",
"whatsNew": null
},
"relationships": {
"appScreenshotSets": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/4bad54bc-3810-401a-a7f4-b02ef8206c41/relationships/appScreenshotSets",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/4bad54bc-3810-401a-a7f4-b02ef8206c41/appScreenshotSets"
}
},
"appPreviewSets": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/4bad54bc-3810-401a-a7f4-b02ef8206c41/relationships/appPreviewSets",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/4bad54bc-3810-401a-a7f4-b02ef8206c41/appPreviewSets"
}
}
},
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/4bad54bc-3810-401a-a7f4-b02ef8206c41"
}
}, {
"type": "appStoreVersionLocalizations",
"id": "61153520-c5df-40a1-bb2b-e09cbfc33fe9",
"attributes": {
"description": "Some of the best apps",
"locale": "en-US",
"keywords": "meme, red, square, x2",
"marketingUrl": "https://github.com/bycedric/red-square-app",
"promotionalText": null,
"supportUrl": "https://github.com/bycedric/red-square-app/issues",
"whatsNew": null
},
"relationships": {
"appScreenshotSets": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/61153520-c5df-40a1-bb2b-e09cbfc33fe9/relationships/appScreenshotSets",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/61153520-c5df-40a1-bb2b-e09cbfc33fe9/appScreenshotSets"
}
},
"appPreviewSets": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/61153520-c5df-40a1-bb2b-e09cbfc33fe9/relationships/appPreviewSets",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/61153520-c5df-40a1-bb2b-e09cbfc33fe9/appPreviewSets"
}
}
},
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/61153520-c5df-40a1-bb2b-e09cbfc33fe9"
}
}],
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/949768f6-58e5-46b4-b046-23861f61831a/appStoreVersionLocalizations"
},
"meta": {
"paging": {
"total": 2,
"limit": 50
}
}
}
Loaded 100 of 159 files, more files were not shown because too many files have changed in this diff. Show more