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

Filter by extension

Filter by extension

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

- [build-tools] Allow expo.test subdomains to open local and staging simulator previews. ([#4480](https://github.com/expo/eas-cli/pull/4480) by [@hirbod](https://github.com/hirbod))
- [build-tools] Retry App Store Connect connection failures and safely recover interrupted status reads and upload commits. ([#4520](https://github.com/expo/eas-cli/pull/4520) by [@sjchmiela](https://github.com/sjchmiela))
- [eas-cli] Say why Apple Developer access is required after all when the optional Apple sign-in was declined, instead of prompting for an Apple ID with no explanation. ([#4444](https://github.com/expo/eas-cli/pull/4444) by [@LizunovSergey](https://github.com/LizunovSergey))

### 🧹 Chores

Expand Down
86 changes: 86 additions & 0 deletions packages/eas-cli/src/credentials/__tests__/context-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { CredentialsContext } from '../context';
import { authenticateAsync } from '../ios/appstore/authenticate';
import Log from '../../log';
import { confirmAsync } from '../../prompts';

jest.mock('../../prompts');
jest.mock('../../log');
jest.mock('../ios/appstore/authenticate');

function createContext(): CredentialsContext {
return new CredentialsContext({
projectInfo: null,
nonInteractive: false,
projectDir: '.',
user: { __typename: 'User', username: 'quin', accounts: [] } as any,
graphqlClient: {} as any,
analytics: {} as any,
vcsClient: {} as any,
});
}

describe(CredentialsContext.prototype.bestEffortAppStoreAuthenticateAsync, () => {
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(authenticateAsync).mockResolvedValue({ team: { id: 'TEAM' } } as any);
});

it('authenticates without further explanation when the offer is accepted', async () => {
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
const ctx = createContext();

await ctx.bestEffortAppStoreAuthenticateAsync();

expect(authenticateAsync).toHaveBeenCalledTimes(1);
expect(Log.warn).not.toHaveBeenCalled();
});

it('says why a later step needs the login that was declined', async () => {
// Repros expo/eas-cli#4422: declining prints "we will ask you again about it", and the next
// step that needs Apple access then goes straight to the Apple ID prompt with no reason
// given, which reads as the decline having been ignored.
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
const ctx = createContext();

await ctx.bestEffortAppStoreAuthenticateAsync();
expect(authenticateAsync).not.toHaveBeenCalled();
expect(Log.warn).not.toHaveBeenCalled();

// A later step that cannot run without Apple access, e.g. listing distribution certificates.
await ctx.appStore.ensureAuthenticatedAsync();

expect(Log.warn).toHaveBeenCalledWith(
expect.stringContaining('the login you skipped is needed after all')
);
expect(authenticateAsync).toHaveBeenCalledTimes(1);
});

it('explains once, not before every later call', async () => {
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
const ctx = createContext();
await ctx.bestEffortAppStoreAuthenticateAsync();

await ctx.appStore.ensureAuthenticatedAsync();
ctx.appStore.authCtx = undefined; // a later step re-authenticates, e.g. after a mode switch
await ctx.appStore.ensureAuthenticatedAsync();

expect(Log.warn).toHaveBeenCalledTimes(1);
});

it('does not prompt or explain in non-interactive mode', async () => {
const ctx = new CredentialsContext({
projectInfo: null,
nonInteractive: true,
projectDir: '.',
user: { __typename: 'User', username: 'quin', accounts: [] } as any,
graphqlClient: {} as any,
analytics: {} as any,
vcsClient: {} as any,
});

await ctx.bestEffortAppStoreAuthenticateAsync();

expect(confirmAsync).not.toHaveBeenCalled();
expect(Log.warn).not.toHaveBeenCalled();
});
});
10 changes: 10 additions & 0 deletions packages/eas-cli/src/credentials/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,16 @@ export class CredentialsContext {
'No problem! 👌 If any of the next steps will require Apple account access we will ask you again about it.'
)
);
// That promise is only half kept: a step that cannot run without Apple access does not ask
// again, it goes straight to the Apple ID prompt, which reads as the decline having been
// ignored (expo/eas-cli#4422). Name the reason when that happens.
this.appStore.onBeforeForcedAuthentication = () => {
this.appStore.onBeforeForcedAuthentication = undefined;
Log.newLine();
Log.warn(
'This step cannot be completed without Apple Developer access, so the login you skipped is needed after all.'
);
};
}
this.shouldAskAuthenticateAppStore = false;
}
Expand Down
7 changes: 7 additions & 0 deletions packages/eas-cli/src/credentials/ios/appstore/AppStoreApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ import Log from '../../../log';
export default class AppStoreApi {
public authCtx?: AuthCtx;
public defaultAuthenticationMode: AuthenticationMode;
/**
* Invoked once, immediately before authentication is forced on a caller that did not ask for
* it. Set by {@link CredentialsContext.bestEffortAppStoreAuthenticateAsync} when the optional
* offer was declined, so the login that follows can say why it is required.
*/
public onBeforeForcedAuthentication?: () => void;

constructor() {
this.defaultAuthenticationMode = hasAscEnvVars()
Expand All @@ -75,6 +81,7 @@ export default class AppStoreApi {

public async ensureAuthenticatedAsync(options?: AuthenticateOptions): Promise<AuthCtx> {
if (!this.authCtx) {
this.onBeforeForcedAuthentication?.();
const mode = options?.mode ?? this.defaultAuthenticationMode;
this.authCtx = await authenticateAsync({ mode, ...options });
}
Expand Down
Loading