Skip to content

Commit 0099dc3

Browse files
authored
feat(passport): ID-3960 Headless login analytics (#2706)
1 parent 055bb49 commit 0099dc3

File tree

8 files changed

+91
-49
lines changed

8 files changed

+91
-49
lines changed

packages/game-bridge/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,8 @@ window.callFunction = async (jsonData: string) => {
362362
case PASSPORT_FUNCTIONS.getPKCEAuthUrl: {
363363
const request = data ? JSON.parse(data) : {};
364364
const directLoginOptions: passport.DirectLoginOptions | undefined = request?.directLoginOptions;
365-
const url = await getPassportClient().loginWithPKCEFlow(directLoginOptions);
365+
const imPassportTraceId: string | undefined = request?.imPassportTraceId;
366+
const url = await getPassportClient().loginWithPKCEFlow(directLoginOptions, imPassportTraceId);
366367
trackDuration(moduleName, 'performedGetPkceAuthUrl', mt(markStart));
367368
callbackToGame({
368369
responseFor: fxName,

packages/passport/sdk/src/Passport.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,11 +266,12 @@ export class Passport {
266266
/**
267267
* Initiates a PKCE flow login.
268268
* @param {DirectLoginOptions} [directLoginOptions] - If provided, directly redirects to the specified login method
269+
* @param {string} [imPassportTraceId] - The trace ID for the PKCE flow
269270
* @returns {string} The authorization URL for the PKCE flow
270271
*/
271-
public loginWithPKCEFlow(directLoginOptions?: DirectLoginOptions): Promise<string> {
272+
public loginWithPKCEFlow(directLoginOptions?: DirectLoginOptions, imPassportTraceId?: string): Promise<string> {
272273
return withMetricsAsync(
273-
async () => await this.authManager.getPKCEAuthorizationUrl(directLoginOptions),
274+
async () => await this.authManager.getPKCEAuthorizationUrl(directLoginOptions, imPassportTraceId),
274275
'loginWithPKCEFlow',
275276
);
276277
}

packages/passport/sdk/src/authManager.test.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,14 @@ describe('AuthManager', () => {
415415
});
416416
const am = new AuthManager(configWithPopupOverlayOptions, mockEmbeddedLoginPrompt);
417417

418+
// Mock the embedded login prompt to return a result
419+
const mockEmbeddedLoginPromptResult = {
420+
directLoginMethod: 'google' as const,
421+
marketingConsentStatus: MarketingConsentStatus.OptedIn,
422+
imPassportTraceId: 'test-trace-id',
423+
};
424+
mockEmbeddedLoginPrompt.displayEmbeddedLoginPrompt.mockResolvedValue(mockEmbeddedLoginPromptResult);
425+
418426
mockSigninPopup.mockReturnValue(mockOidcUser);
419427
// Simulate `tryAgainOnClick` being called so that the `login()` promise can resolve
420428
mockOverlayAppend.mockImplementation(async (tryAgainOnClick: () => Promise<void>) => {
@@ -1080,6 +1088,13 @@ describe('AuthManager', () => {
10801088
expect(url.searchParams.get('marketingConsent')).toEqual(MarketingConsentStatus.OptedIn);
10811089
expect(url.searchParams.get('audience')).toEqual('test-audience');
10821090
});
1091+
1092+
it('should include im_passport_trace_id parameter when imPassportTraceId is provided', async () => {
1093+
const result = await authManager.getPKCEAuthorizationUrl(undefined, 'test-trace-id');
1094+
const url = new URL(result);
1095+
1096+
expect(url.searchParams.get('im_passport_trace_id')).toEqual('test-trace-id');
1097+
});
10831098
});
10841099

10851100
describe('login with directLoginMethod', () => {
@@ -1178,8 +1193,12 @@ describe('AuthManager', () => {
11781193
});
11791194

11801195
it('should call displayEmbeddedLoginPrompt when no directLoginOptions provided and overlay is enabled', async () => {
1181-
const mockDirectLoginOptions = { directLoginMethod: 'google' as const };
1182-
mockEmbeddedLoginPrompt.displayEmbeddedLoginPrompt.mockResolvedValue(mockDirectLoginOptions);
1196+
const mockEmbeddedLoginPromptResult = {
1197+
directLoginMethod: 'google' as const,
1198+
marketingConsentStatus: MarketingConsentStatus.OptedIn,
1199+
imPassportTraceId: 'test-trace-id',
1200+
};
1201+
mockEmbeddedLoginPrompt.displayEmbeddedLoginPrompt.mockResolvedValue(mockEmbeddedLoginPromptResult);
11831202
mockSigninPopup.mockResolvedValue(mockOidcUser);
11841203

11851204
await authManager.login();
@@ -1190,6 +1209,8 @@ describe('AuthManager', () => {
11901209
rid: '',
11911210
third_party_a_id: '',
11921211
direct: 'google',
1212+
marketingConsent: MarketingConsentStatus.OptedIn,
1213+
im_passport_trace_id: 'test-trace-id',
11931214
},
11941215
popupWindowFeatures: {
11951216
width: 410,
@@ -1234,12 +1255,13 @@ describe('AuthManager', () => {
12341255
});
12351256

12361257
it('should handle email login method from embedded prompt', async () => {
1237-
const mockDirectLoginOptions = {
1258+
const mockEmbeddedLoginPromptResult = {
12381259
directLoginMethod: 'email' as const,
12391260
12401261
marketingConsentStatus: MarketingConsentStatus.OptedIn,
1262+
imPassportTraceId: 'test-trace-id-email',
12411263
};
1242-
mockEmbeddedLoginPrompt.displayEmbeddedLoginPrompt.mockResolvedValue(mockDirectLoginOptions);
1264+
mockEmbeddedLoginPrompt.displayEmbeddedLoginPrompt.mockResolvedValue(mockEmbeddedLoginPromptResult);
12431265
mockSigninPopup.mockResolvedValue(mockOidcUser);
12441266

12451267
await authManager.login('anonymous-id');
@@ -1252,6 +1274,7 @@ describe('AuthManager', () => {
12521274
direct: 'email',
12531275
12541276
marketingConsent: MarketingConsentStatus.OptedIn,
1277+
im_passport_trace_id: 'test-trace-id-email',
12551278
},
12561279
popupWindowFeatures: {
12571280
width: 410,

packages/passport/sdk/src/authManager.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,11 @@ export default class AuthManager {
183183
});
184184
};
185185

186-
private buildExtraQueryParams(anonymousId?: string, directLoginOptions?: DirectLoginOptions): Record<string, string> {
186+
private buildExtraQueryParams(
187+
anonymousId?: string,
188+
directLoginOptions?: DirectLoginOptions,
189+
imPassportTraceId?: string,
190+
): Record<string, string> {
187191
const params: Record<string, string> = {
188192
...(this.userManager.settings?.extraQueryParams ?? {}),
189193
rid: getDetail(Detail.RUNTIME_ID) || '',
@@ -208,6 +212,10 @@ export default class AuthManager {
208212
}
209213
}
210214

215+
if (imPassportTraceId) {
216+
params.im_passport_trace_id = imPassportTraceId;
217+
}
218+
211219
return params;
212220
}
213221

@@ -232,16 +240,24 @@ export default class AuthManager {
232240
*/
233241
public async login(anonymousId?: string, directLoginOptions?: DirectLoginOptions): Promise<User> {
234242
return withPassportError<User>(async () => {
243+
// If directLoginOptions are provided, then the consumer has rendered their own initial login screen.
244+
// If not, display the embedded login prompt and pass the returned direct login options and imPassportTraceId to the login popup.
235245
let directLoginOptionsToUse: DirectLoginOptions | undefined;
246+
let imPassportTraceId: string | undefined;
236247
if (directLoginOptions) {
237248
directLoginOptionsToUse = directLoginOptions;
238249
} else if (!this.config.popupOverlayOptions.disableHeadlessLoginPromptOverlay) {
239-
directLoginOptionsToUse = await this.embeddedLoginPrompt.displayEmbeddedLoginPrompt();
250+
const {
251+
imPassportTraceId: embeddedLoginPromptImPassportTraceId,
252+
...embeddedLoginPromptDirectLoginOptions
253+
} = await this.embeddedLoginPrompt.displayEmbeddedLoginPrompt();
254+
directLoginOptionsToUse = embeddedLoginPromptDirectLoginOptions;
255+
imPassportTraceId = embeddedLoginPromptImPassportTraceId;
240256
}
241257

242258
const popupWindowTarget = window.crypto.randomUUID();
243259
const signinPopup = async () => {
244-
const extraQueryParams = this.buildExtraQueryParams(anonymousId, directLoginOptionsToUse);
260+
const extraQueryParams = this.buildExtraQueryParams(anonymousId, directLoginOptionsToUse, imPassportTraceId);
245261

246262
const userPromise = this.userManager.signinPopup({
247263
extraQueryParams,
@@ -380,7 +396,10 @@ export default class AuthManager {
380396
}, PassportErrorType.AUTHENTICATION_ERROR);
381397
}
382398

383-
public async getPKCEAuthorizationUrl(directLoginOptions?: DirectLoginOptions): Promise<string> {
399+
public async getPKCEAuthorizationUrl(
400+
directLoginOptions?: DirectLoginOptions,
401+
imPassportTraceId?: string,
402+
): Promise<string> {
384403
const verifier = base64URLEncode(window.crypto.getRandomValues(new Uint8Array(32)));
385404
const challenge = base64URLEncode(await sha256(verifier));
386405

@@ -421,6 +440,10 @@ export default class AuthManager {
421440
}
422441
}
423442

443+
if (imPassportTraceId) {
444+
pKCEAuthorizationUrl.searchParams.set('im_passport_trace_id', imPassportTraceId);
445+
}
446+
424447
return pKCEAuthorizationUrl.toString();
425448
}
426449

packages/passport/sdk/src/confirmation/embeddedLoginPrompt.test.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
EmbeddedLoginPromptReceiveMessage,
77
EmbeddedLoginPromptResult,
88
} from './types';
9-
import { DirectLoginOptions, MarketingConsentStatus } from '../types';
9+
import { MarketingConsentStatus } from '../types';
1010

1111
// Mock dependencies
1212
jest.mock('../overlay/embeddedLoginPromptOverlay');
@@ -63,7 +63,7 @@ describe('EmbeddedLoginPrompt', () => {
6363
describe('getHref', () => {
6464
it('should generate correct href with client ID', () => {
6565
const href = (embeddedLoginPrompt as any).getHref();
66-
expect(href).toBe(`https://auth.immutable.com/im-embedded-login-prompt?client_id=${mockClientId}`);
66+
expect(href).toBe(`https://auth.immutable.com/im-embedded-login-prompt?client_id=${mockClientId}&rid=undefined`);
6767
});
6868
});
6969

@@ -160,6 +160,7 @@ describe('EmbeddedLoginPrompt', () => {
160160
directLoginMethod: 'email',
161161
162162
marketingConsentStatus: MarketingConsentStatus.OptedIn,
163+
imPassportTraceId: 'test-im-passport-trace-id',
163164
};
164165

165166
const promise = embeddedLoginPrompt.displayEmbeddedLoginPrompt();
@@ -178,10 +179,11 @@ describe('EmbeddedLoginPrompt', () => {
178179
messageHandler(mockEvent);
179180

180181
const result = await promise;
181-
const expectedResult: DirectLoginOptions = {
182+
const expectedResult: EmbeddedLoginPromptResult = {
182183
directLoginMethod: 'email',
183184
marketingConsentStatus: MarketingConsentStatus.OptedIn,
184185
186+
imPassportTraceId: 'test-im-passport-trace-id',
185187
};
186188

187189
expect(result).toEqual(expectedResult);
@@ -193,6 +195,7 @@ describe('EmbeddedLoginPrompt', () => {
193195
const mockLoginResult: EmbeddedLoginPromptResult = {
194196
directLoginMethod: 'google',
195197
marketingConsentStatus: MarketingConsentStatus.Unsubscribed,
198+
imPassportTraceId: 'test-im-passport-trace-id',
196199
};
197200

198201
const promise = embeddedLoginPrompt.displayEmbeddedLoginPrompt();
@@ -211,9 +214,10 @@ describe('EmbeddedLoginPrompt', () => {
211214
messageHandler(mockEvent);
212215

213216
const result = await promise;
214-
const expectedResult: DirectLoginOptions = {
217+
const expectedResult: EmbeddedLoginPromptResult = {
215218
directLoginMethod: 'google',
216219
marketingConsentStatus: MarketingConsentStatus.Unsubscribed,
220+
imPassportTraceId: 'test-im-passport-trace-id',
217221
};
218222

219223
expect(result).toEqual(expectedResult);
@@ -276,7 +280,7 @@ describe('EmbeddedLoginPrompt', () => {
276280

277281
messageHandler(mockEvent);
278282

279-
await expect(promise).rejects.toThrow('Unsupported message type');
283+
await expect(promise).rejects.toThrow('Unsupported message type: UNKNOWN_MESSAGE_TYPE');
280284
expect(mockRemoveEventListener).toHaveBeenCalledWith('message', messageHandler);
281285
expect(mockOverlay.remove).toHaveBeenCalled();
282286
});

packages/passport/sdk/src/confirmation/embeddedLoginPrompt.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
import { Detail, getDetail } from '@imtbl/metrics';
12
import {
23
EMBEDDED_LOGIN_PROMPT_EVENT_TYPE,
34
EmbeddedLoginPromptResult,
45
EmbeddedLoginPromptReceiveMessage,
56
} from './types';
67
import { PassportConfiguration } from '../config';
78
import EmbeddedLoginPromptOverlay from '../overlay/embeddedLoginPromptOverlay';
8-
import { DirectLoginOptions } from '../types';
99

1010
const LOGIN_PROMPT_WINDOW_HEIGHT = 560;
1111
const LOGIN_PROMPT_WINDOW_WIDTH = 440;
@@ -21,7 +21,9 @@ export default class EmbeddedLoginPrompt {
2121
}
2222

2323
private getHref = () => (
24-
`${this.config.authenticationDomain}/im-embedded-login-prompt?client_id=${this.config.oidcConfiguration.clientId}`
24+
`${this.config.authenticationDomain}/im-embedded-login-prompt`
25+
+ `?client_id=${this.config.oidcConfiguration.clientId}`
26+
+ `&rid=${getDetail(Detail.RUNTIME_ID)}`
2527
);
2628

2729
private static appendIFrameStylesIfNeeded = () => {
@@ -89,7 +91,7 @@ export default class EmbeddedLoginPrompt {
8991
return embeddedLoginPrompt;
9092
};
9193

92-
public displayEmbeddedLoginPrompt(): Promise<DirectLoginOptions> {
94+
public displayEmbeddedLoginPrompt(): Promise<EmbeddedLoginPromptResult> {
9395
return new Promise((resolve, reject) => {
9496
const embeddedLoginPrompt = this.getEmbeddedLoginIFrame();
9597
const messageHandler = ({ data, origin }: MessageEvent) => {
@@ -102,20 +104,7 @@ export default class EmbeddedLoginPrompt {
102104

103105
switch (data.messageType as EmbeddedLoginPromptReceiveMessage) {
104106
case EmbeddedLoginPromptReceiveMessage.LOGIN_METHOD_SELECTED: {
105-
const loginMethod = data.payload as EmbeddedLoginPromptResult;
106-
let result: DirectLoginOptions;
107-
if (loginMethod.directLoginMethod === 'email') {
108-
result = {
109-
directLoginMethod: 'email',
110-
marketingConsentStatus: loginMethod.marketingConsentStatus,
111-
email: loginMethod.email,
112-
};
113-
} else {
114-
result = {
115-
directLoginMethod: loginMethod.directLoginMethod,
116-
marketingConsentStatus: loginMethod.marketingConsentStatus,
117-
};
118-
}
107+
const result = data.payload as EmbeddedLoginPromptResult;
119108
window.removeEventListener('message', messageHandler);
120109
EmbeddedLoginPromptOverlay.remove();
121110
resolve(result);
@@ -136,7 +125,7 @@ export default class EmbeddedLoginPrompt {
136125
default:
137126
window.removeEventListener('message', messageHandler);
138127
EmbeddedLoginPromptOverlay.remove();
139-
reject(new Error('Unsupported message type'));
128+
reject(new Error(`Unsupported message type: ${data.messageType}`));
140129
break;
141130
}
142131
};

packages/passport/sdk/src/confirmation/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export type ConfirmationResult = {
2626

2727
export type EmbeddedLoginPromptResult = {
2828
marketingConsentStatus: MarketingConsentStatus;
29+
imPassportTraceId: string;
2930
} & (
3031
| { directLoginMethod: 'email'; email: string }
3132
| { directLoginMethod: Exclude<DirectLoginMethod, 'email'>; email?: never }

packages/passport/sdk/src/overlay/elements.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -133,28 +133,28 @@ export const getEmbeddedLoginPromptOverlay = (): string => `
133133
<div
134134
id="${PASSPORT_OVERLAY_ID}"
135135
style="
136-
position: fixed !important;
137-
top: 0 !important;
138-
left: 0 !important;
139-
width: 100% !important;
140-
height: 100% !important;
141-
display: flex !important;
142-
flex-direction: column !important;
143-
justify-content: center !important;
144-
align-items: center !important;
145-
z-index: 2147483647 !important;
146-
background: rgba(247, 247, 247, 0.24) !important;
136+
position: fixed;
137+
top: 0;
138+
left: 0;
139+
width: 100%;
140+
height: 100%;
141+
display: flex;
142+
flex-direction: column;
143+
justify-content: center;
144+
align-items: center;
145+
z-index: 2147483647;
146+
background: rgba(247, 247, 247, 0.24);
147147
animation-name: passportEmbeddedLoginPromptOverlayFadeIn;
148148
animation-duration: 0.8s;
149149
"
150150
>
151151
<div
152152
id="${PASSPORT_OVERLAY_CONTENTS_ID}"
153153
style="
154-
display: flex !important;
155-
flex-direction: column !important;
156-
align-items: center !important;
157-
width: 100% !important;
154+
display: flex;
155+
flex-direction: column;
156+
align-items: center;
157+
width: 100%;
158158
"
159159
/>
160160
</div>

0 commit comments

Comments
 (0)