Skip to content

Commit 484bdd6

Browse files
authored
Merge pull request #3872 from codecrafters-io/tropicolx/lobbyside-other-emails-v2
Push non-primary emails to Lobbyside (other_emails column) — take 2
2 parents 0f74fce + fd841fe commit 484bdd6

6 files changed

Lines changed: 109 additions & 0 deletions

File tree

app/components/lobbyside-widget/index.hbs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
this.customFields.stages_completed
2626
this.customFields.signed_up_date
2727
this.customFields.subscription_type
28+
this.customFields.other_emails
2829
}}
2930
{{will-destroy this.removeScript}}
3031
></div>

app/components/lobbyside-widget/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ declare global {
1515
stages_completed: string;
1616
signed_up_date: string;
1717
subscription_type: string;
18+
other_emails: string;
1819
}): void;
1920
};
2021
}
@@ -30,12 +31,15 @@ interface VisitorSource {
3031
hasActiveSubscription?: boolean;
3132
teamHasActiveSubscription?: boolean;
3233
courseParticipations?: { completedStageSlugs?: string[] }[];
34+
primaryEmailAddress?: string | null;
35+
emailAddresses?: { value?: string | null }[];
3336
}
3437

3538
export interface LobbysideCustomFields {
3639
stages_completed: string;
3740
signed_up_date: string;
3841
subscription_type: string;
42+
other_emails: string;
3943
}
4044

4145
function subscriptionType(user: VisitorSource | null | undefined): string {
@@ -62,6 +66,21 @@ function subscriptionType(user: VisitorSource | null | undefined): string {
6266
return 'free';
6367
}
6468

69+
function otherEmails(user: VisitorSource | null | undefined): string {
70+
const primary = (user?.primaryEmailAddress ?? '').trim();
71+
const seen = new Set<string>();
72+
73+
for (const entry of user?.emailAddresses ?? []) {
74+
const value = (entry?.value ?? '').trim();
75+
76+
if (value && value !== primary) {
77+
seen.add(value);
78+
}
79+
}
80+
81+
return Array.from(seen).join(', ');
82+
}
83+
6584
export function lobbysideCustomFields(user: VisitorSource | null | undefined): LobbysideCustomFields {
6685
const stagesCompleted = (user?.courseParticipations ?? []).reduce(
6786
(sum, participation) => sum + (participation.completedStageSlugs?.length ?? 0),
@@ -72,6 +91,7 @@ export function lobbysideCustomFields(user: VisitorSource | null | undefined): L
7291
stages_completed: String(stagesCompleted),
7392
signed_up_date: user?.createdAt ? user.createdAt.toISOString().slice(0, 10) : '',
7493
subscription_type: subscriptionType(user),
94+
other_emails: otherEmails(user),
7595
};
7696
}
7797

app/services/authenticator.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,5 +152,8 @@ export default class AuthenticatorService extends Service {
152152
this.prefillBeaconEmail();
153153
this.currentUserCacheStorage.setValues(user.id, user.username);
154154
this.cacheBuster++;
155+
156+
// Populates currentUser.emailAddresses for the Lobbyside `other_emails` field.
157+
this.store.findAll('email-address', { include: 'user' }).catch(() => {});
155158
}
156159
}

tests/support/verify-api-requests.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export default class ApiRequestsVerifier {
2020
pathname !== '/api/v1/analytics-events' &&
2121
// Triggered on application boot
2222
pathname !== '/api/v1/users/current' &&
23+
// Triggered on application boot (Lobbyside widget other_emails)
24+
pathname !== '/api/v1/email-addresses' &&
2325
// Triggered when header is rendered
2426
!pathname.match(/^\/api\/v1\/users\/[^/]+\/top-language-leaderboard-slugs$/)
2527
);

tests/unit/components/lobbyside-widget-test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ module('Unit | Component | lobbyside-widget', function () {
2020
stages_completed: '0',
2121
signed_up_date: '',
2222
subscription_type: '',
23+
other_emails: '',
2324
});
2425
});
2526

@@ -57,4 +58,43 @@ module('Unit | Component | lobbyside-widget', function () {
5758

5859
assert.strictEqual(lobbysideCustomFields(createUser()).subscription_type, 'free');
5960
});
61+
62+
test('other_emails joins every non-primary email, comma-separated', function (assert) {
63+
const user = createUser({
64+
primaryEmailAddress: 'primary@example.com',
65+
emailAddresses: [{ value: 'primary@example.com' }, { value: 'work@example.com' }, { value: 'school@example.com' }],
66+
} as unknown as Partial<UserModel>);
67+
68+
assert.strictEqual(lobbysideCustomFields(user).other_emails, 'work@example.com, school@example.com');
69+
});
70+
71+
test('other_emails excludes the primary email and is empty when it is the only one', function (assert) {
72+
const user = createUser({
73+
primaryEmailAddress: 'primary@example.com',
74+
emailAddresses: [{ value: 'primary@example.com' }],
75+
} as unknown as Partial<UserModel>);
76+
77+
assert.strictEqual(lobbysideCustomFields(user).other_emails, '');
78+
});
79+
80+
test('other_emails dedupes and drops blank or missing values', function (assert) {
81+
const user = createUser({
82+
primaryEmailAddress: 'primary@example.com',
83+
emailAddresses: [
84+
{ value: 'work@example.com' },
85+
{ value: ' work@example.com ' },
86+
{ value: '' },
87+
{ value: null },
88+
{ value: 'alt@example.com' },
89+
],
90+
} as unknown as Partial<UserModel>);
91+
92+
assert.strictEqual(lobbysideCustomFields(user).other_emails, 'work@example.com, alt@example.com');
93+
});
94+
95+
test('other_emails is empty when the user has no email addresses', function (assert) {
96+
const user = createUser({ primaryEmailAddress: 'primary@example.com', emailAddresses: [] } as unknown as Partial<UserModel>);
97+
98+
assert.strictEqual(lobbysideCustomFields(user).other_emails, '');
99+
});
60100
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Response } from 'miragejs';
2+
import { module, test } from 'qunit';
3+
import { setupMirage } from 'codecrafters-frontend/tests/test-support/mirage';
4+
import { setupTest } from 'codecrafters-frontend/tests/helpers';
5+
import { setupWindowMock } from 'ember-window-mock/test-support';
6+
import { signIn } from 'codecrafters-frontend/tests/support/authentication-helpers';
7+
import testScenario from 'codecrafters-frontend/mirage/scenarios/test';
8+
9+
module('Unit | Service | authenticator', function (hooks) {
10+
setupTest(hooks);
11+
setupMirage(hooks);
12+
setupWindowMock(hooks);
13+
14+
test('syncCurrentUser still loads the user when the email-addresses endpoint fails', async function (assert) {
15+
testScenario(this.server);
16+
const user = signIn(this.owner, this.server);
17+
18+
// Regression: an `email-addresses` failure must never break the auth bootstrap (LSD revert of #3869).
19+
this.server.get('/email-addresses', () => new Response(500, {}, { errors: [{ detail: 'boom' }] }));
20+
21+
const authenticator = this.owner.lookup('service:authenticator');
22+
await authenticator.syncCurrentUser();
23+
24+
assert.ok(authenticator.currentUser, 'current user is loaded despite the failing endpoint');
25+
assert.strictEqual(authenticator.currentUser.id, user.id, 'the signed-in user is the current user');
26+
});
27+
28+
test('syncCurrentUser populates currentUser.emailAddresses for the Lobbyside widget', async function (assert) {
29+
testScenario(this.server);
30+
const user = signIn(this.owner, this.server);
31+
32+
this.server.schema.emailAddresses.create({ user, value: 'work@example.com' });
33+
this.server.schema.emailAddresses.create({ user, value: 'alt@example.com' });
34+
35+
const authenticator = this.owner.lookup('service:authenticator');
36+
await authenticator.syncCurrentUser();
37+
await this.owner.lookup('service:store').findAll('email-address', { include: 'user' });
38+
39+
const values = authenticator.currentUser.emailAddresses.map((emailAddress) => emailAddress.value);
40+
assert.true(values.includes('work@example.com'), 'secondary email is attached to the current user');
41+
assert.true(values.includes('alt@example.com'), 'all secondary emails are attached to the current user');
42+
});
43+
});

0 commit comments

Comments
 (0)