Skip to content

Commit 508a6a4

Browse files
committed
[eas-cli] Fix Convex invite output
1 parent 51311f1 commit 508a6a4

14 files changed

Lines changed: 120 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This is the log of notable changes to EAS CLI and related packages.
1010

1111
### 🐛 Bug fixes
1212

13+
- [eas-cli] Fix Convex team invite output after skipped or unnecessary invitations. ([#3672](https://github.com/expo/eas-cli/pull/3672) by [@fiberjw](https://github.com/fiberjw))
14+
1315
### 🧹 Chores
1416

1517
## [18.10.0](https://github.com/expo/eas-cli/releases/tag/v18.10.0) - 2026-05-04

packages/eas-cli/src/commandUtils/__tests__/convex-test.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import dateFormat from 'dateformat';
2+
13
import {
24
confirmRecentConvexInviteAsync,
35
formatConvexProject,
@@ -22,6 +24,7 @@ describe('Convex command utilities', () => {
2224
convexTeamIdentifier: 'team-123',
2325
convexTeamName: 'Test Team',
2426
convexTeamSlug: 'test team',
27+
hasBeenClaimed: true,
2528
createdAt: '2024-01-01T00:00:00.000Z',
2629
updatedAt: '2024-01-01T00:00:00.000Z',
2730
invitedAt: '2024-01-02T00:00:00.000Z',
@@ -58,8 +61,12 @@ describe('Convex command utilities', () => {
5861
expect(formatConvexTeamConnection(mockConnection)).toContain('Test Team / test team');
5962
expect(formatConvexTeamConnection(mockConnection)).not.toContain('team-123');
6063
expect(formatConvexTeamConnection(mockConnection)).not.toContain('connection-1');
64+
expect(formatConvexTeamConnection(mockConnection)).toContain('Claimed');
65+
expect(formatConvexTeamConnection(mockConnection)).toContain('Yes');
6166
expect(formatConvexTeamConnection(mockConnection)).toContain('user@example.com');
62-
expect(formatConvexTeamConnection(mockConnection)).toContain('2024-01-02T00:00:00.000Z');
67+
expect(formatConvexTeamConnection(mockConnection)).toContain(
68+
dateFormat(mockConnection.invitedAt, 'mmm dd HH:MM:ss')
69+
);
6370
expect(formatConvexProject(mockProject)).toContain('project-123');
6471
expect(formatConvexProject(mockProject)).not.toContain('convex-project-1');
6572
expect(formatConvexProject(mockProject)).toContain('Test Team / test team');
@@ -93,12 +100,13 @@ describe('Convex command utilities', () => {
93100

94101
it('prompts before resending a recent invite in interactive mode', async () => {
95102
jest.mocked(confirmAsync).mockResolvedValue(false);
103+
const recentInviteTimestamp = new Date(Date.now() - 5 * 60 * 1000).toISOString();
96104

97105
await expect(
98106
confirmRecentConvexInviteAsync(
99107
{
100108
...mockConnection,
101-
invitedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
109+
invitedAt: recentInviteTimestamp,
102110
},
103111
{ nonInteractive: false }
104112
)
@@ -107,6 +115,9 @@ describe('Convex command utilities', () => {
107115
expect(confirmAsync).toHaveBeenCalledWith({
108116
message: expect.stringContaining('Are you sure you want to send another invite?'),
109117
});
118+
expect(confirmAsync).toHaveBeenCalledWith({
119+
message: expect.stringContaining(dateFormat(recentInviteTimestamp, 'mmm dd HH:MM:ss')),
120+
});
110121
});
111122

112123
it('warns and proceeds for recent invites in non-interactive mode', async () => {

packages/eas-cli/src/commandUtils/convex.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import chalk from 'chalk';
2+
import dateFormat from 'dateformat';
23

34
import { ConvexProjectData, ConvexTeamConnectionData } from '../graphql/types/ConvexTeamConnection';
45
import Log, { link } from '../log';
@@ -7,6 +8,10 @@ import { confirmAsync } from '../prompts';
78
const CONVEX_DASHBOARD_HOST = 'https://dashboard.convex.dev';
89
const RECENT_INVITE_THRESHOLD_MS = 60 * 60 * 1000;
910

11+
export function formatConvexInviteTimestamp(timestamp: string): string {
12+
return dateFormat(timestamp, 'mmm dd HH:MM:ss');
13+
}
14+
1015
export function getConvexTeamDashboardUrl(connection: ConvexTeamConnectionData): string {
1116
return `${CONVEX_DASHBOARD_HOST}/t/${encodeURIComponent(connection.convexTeamSlug)}`;
1217
}
@@ -25,13 +30,14 @@ export function formatConvexTeamConnection(connection: ConvexTeamConnectionData)
2530
const lines = [
2631
`${chalk.bold('Team')}: ${formatConvexTeam(connection)}`,
2732
`${chalk.bold('Dashboard')}: ${link(getConvexTeamDashboardUrl(connection), { dim: false })}`,
33+
`${chalk.bold('Claimed')}: ${connection.hasBeenClaimed ? 'Yes' : 'No'}`,
2834
];
2935

3036
if (connection.invitedEmail) {
3137
lines.push(`${chalk.bold('Invited email')}: ${connection.invitedEmail}`);
3238
}
3339
if (connection.invitedAt) {
34-
lines.push(`${chalk.bold('Invited at')}: ${connection.invitedAt}`);
40+
lines.push(`${chalk.bold('Invited at')}: ${formatConvexInviteTimestamp(connection.invitedAt)}`);
3541
}
3642

3743
return lines.join('\n');
@@ -69,7 +75,7 @@ export async function confirmRecentConvexInviteAsync(
6975
return true;
7076
}
7177

72-
const previousInvite = `A Convex team invite was already sent${connection.invitedEmail ? ` to ${connection.invitedEmail}` : ''} at ${connection.invitedAt}.`;
78+
const previousInvite = `A Convex team invite was already sent${connection.invitedEmail ? ` to ${connection.invitedEmail}` : ''} at ${formatConvexInviteTimestamp(connection.invitedAt)}.`;
7379
if (nonInteractive) {
7480
Log.warn(
7581
`${previousInvite} Sending another invite because this command is running in non-interactive mode.`

packages/eas-cli/src/commands/integrations/convex/__tests__/connect.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ describe(IntegrationsConvexConnect, () => {
7575
convexTeamIdentifier: 'team-123',
7676
convexTeamName: 'Test Team',
7777
convexTeamSlug: 'test-team',
78+
hasBeenClaimed: false,
7879
createdAt: '2024-01-01T00:00:00.000Z',
7980
updatedAt: '2024-01-01T00:00:00.000Z',
8081
invitedAt: null,
@@ -153,6 +154,9 @@ describe(IntegrationsConvexConnect, () => {
153154
graphqlClient,
154155
{ convexTeamConnectionId: 'connection-1' }
155156
);
157+
expect(Log.log).toHaveBeenCalledWith(
158+
expect.stringContaining('Check your email for an invitation')
159+
);
156160
expect(spawnAsync).toHaveBeenCalledWith('npx', ['expo', 'install', 'convex'], {
157161
cwd: testProjectDir,
158162
stdio: 'inherit',
@@ -237,6 +241,32 @@ describe(IntegrationsConvexConnect, () => {
237241
});
238242
expect(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync).not.toHaveBeenCalled();
239243
expect(Log.warn).toHaveBeenCalledWith('Skipped sending Convex team invitation.');
244+
expect(Log.log).not.toHaveBeenCalledWith(
245+
expect.stringContaining('Check your email for an invitation')
246+
);
247+
});
248+
249+
it('skips sending an invite when the Convex team has already been claimed', async () => {
250+
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([
251+
{
252+
...mockConnection,
253+
hasBeenClaimed: true,
254+
invitedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
255+
},
256+
]);
257+
258+
await createCommand([]).runAsync();
259+
260+
expect(confirmAsync).not.toHaveBeenCalledWith({
261+
message: expect.stringContaining('Are you sure you want to send another invite?'),
262+
});
263+
expect(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync).not.toHaveBeenCalled();
264+
expect(Log.warn).toHaveBeenCalledWith(
265+
'Convex team has already been claimed. Skipping Convex team invitation.'
266+
);
267+
expect(Log.log).not.toHaveBeenCalledWith(
268+
expect.stringContaining('Check your email for an invitation')
269+
);
240270
});
241271

242272
it('writes the deploy key and Convex URL to .env.local', async () => {

packages/eas-cli/src/commands/integrations/convex/__tests__/dashboard.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe(IntegrationsConvexDashboard, () => {
3232
convexTeamIdentifier: 'team-123',
3333
convexTeamName: 'Test Team',
3434
convexTeamSlug: 'test team',
35+
hasBeenClaimed: false,
3536
createdAt: '2024-01-01T00:00:00.000Z',
3637
updatedAt: '2024-01-01T00:00:00.000Z',
3738
invitedAt: null,

packages/eas-cli/src/commands/integrations/convex/__tests__/project-delete.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe(IntegrationsConvexProjectDelete, () => {
3232
convexTeamIdentifier: 'team-123',
3333
convexTeamName: 'Test Team',
3434
convexTeamSlug: 'test-team',
35+
hasBeenClaimed: false,
3536
createdAt: '2024-01-01T00:00:00.000Z',
3637
updatedAt: '2024-01-01T00:00:00.000Z',
3738
invitedAt: null,

packages/eas-cli/src/commands/integrations/convex/__tests__/project.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ describe(IntegrationsConvexProject, () => {
2121
convexTeamIdentifier: 'team-123',
2222
convexTeamName: 'Test Team',
2323
convexTeamSlug: 'test-team',
24+
hasBeenClaimed: false,
2425
createdAt: '2024-01-01T00:00:00.000Z',
2526
updatedAt: '2024-01-01T00:00:00.000Z',
2627
invitedAt: null,

packages/eas-cli/src/commands/integrations/convex/__tests__/team-delete.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ describe(IntegrationsConvexTeamDelete, () => {
4040
convexTeamIdentifier: 'team-123',
4141
convexTeamName: 'Test Team',
4242
convexTeamSlug: 'test-team',
43+
hasBeenClaimed: false,
4344
createdAt: '2024-01-01T00:00:00.000Z',
4445
updatedAt: '2024-01-01T00:00:00.000Z',
4546
invitedAt: '2024-01-02T00:00:00.000Z',

packages/eas-cli/src/commands/integrations/convex/__tests__/team-invite.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import dateFormat from 'dateformat';
2+
13
import { getMockOclifConfig } from '../../../../__tests__/commands/utils';
24
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
35
import { testProjectId } from '../../../../credentials/__tests__/fixtures-constants';
@@ -47,6 +49,7 @@ describe(IntegrationsConvexTeamInvite, () => {
4749
convexTeamIdentifier: 'team-123',
4850
convexTeamName: 'Test Team',
4951
convexTeamSlug: 'test-team',
52+
hasBeenClaimed: false,
5053
createdAt: '2024-01-01T00:00:00.000Z',
5154
updatedAt: '2024-01-01T00:00:00.000Z',
5255
invitedAt: '2024-01-02T00:00:00.000Z',
@@ -98,8 +101,12 @@ describe(IntegrationsConvexTeamInvite, () => {
98101
expect(Log.log).not.toHaveBeenCalledWith(expect.stringContaining('team-123'));
99102
expect(Log.log).not.toHaveBeenCalledWith(expect.stringContaining('connection-1'));
100103
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('Previous invite'));
104+
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('Claimed'));
105+
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('No'));
101106
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('previous@example.com'));
102-
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('2024-01-02T00:00:00.000Z'));
107+
expect(Log.log).toHaveBeenCalledWith(
108+
expect.stringContaining(dateFormat(mockConnection.invitedAt, 'mmm dd HH:MM:ss'))
109+
);
103110
});
104111

105112
it('prompts before resending a recent invite', async () => {
@@ -133,6 +140,24 @@ describe(IntegrationsConvexTeamInvite, () => {
133140
expect(Log.warn).toHaveBeenCalledWith('Skipped sending Convex team invitation.');
134141
});
135142

143+
it('skips sending an invite when the Convex team has already been claimed', async () => {
144+
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([
145+
{
146+
...mockConnection,
147+
hasBeenClaimed: true,
148+
invitedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
149+
},
150+
]);
151+
152+
await createCommand([]).runAsync();
153+
154+
expect(confirmAsync).not.toHaveBeenCalled();
155+
expect(ConvexMutation.sendConvexTeamInviteToVerifiedEmailAsync).not.toHaveBeenCalled();
156+
expect(Log.warn).toHaveBeenCalledWith(
157+
'Convex team has already been claimed. Skipping Convex team invitation.'
158+
);
159+
});
160+
136161
it('resends recent invites in non-interactive mode with a warning', async () => {
137162
jest.mocked(ConvexQuery.getConvexTeamConnectionsByAccountIdAsync).mockResolvedValue([
138163
{

packages/eas-cli/src/commands/integrations/convex/__tests__/team.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ describe(IntegrationsConvexTeam, () => {
2929
convexTeamIdentifier: 'team-123',
3030
convexTeamName: 'Test Team',
3131
convexTeamSlug: 'test-team',
32+
hasBeenClaimed: true,
3233
createdAt: '2024-01-01T00:00:00.000Z',
3334
updatedAt: '2024-01-01T00:00:00.000Z',
3435
invitedAt: null,
@@ -65,6 +66,8 @@ describe(IntegrationsConvexTeam, () => {
6566
expect.stringContaining('Convex teams linked to @testuser')
6667
);
6768
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('Test Team / test-team'));
69+
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('Claimed'));
70+
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('Yes'));
6871
expect(Log.log).not.toHaveBeenCalledWith(expect.stringContaining('team-123'));
6972
});
7073

0 commit comments

Comments
 (0)