Skip to content

Commit 602b7c1

Browse files
authored
Merge pull request #19447 from mozilla/FXA-12370
feat(aimode): Enable Fx client_id + service=aimode to send web channel messages
2 parents 74ffb25 + ee4769e commit 602b7c1

22 files changed

Lines changed: 363 additions & 71 deletions

File tree

packages/fxa-settings/src/lib/channels/firefox.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -93,19 +93,26 @@ export type FxALoginRequest = {
9393
keyFetchToken?: hexstring;
9494
unwrapBKey?: string;
9595
verifiedCanLinkAccount?: boolean;
96-
services?:
97-
| {
98-
sync: {
99-
offeredEngines?: string[];
100-
declinedEngines?: string[];
101-
};
102-
}
103-
// For sync optional flows (currently only Relay)
104-
| {
105-
relay: {};
106-
};
96+
services?: WebChannelServices;
97+
};
98+
99+
export type SyncEngines = {
100+
offeredEngines?: string[];
101+
declinedEngines?: string[];
107102
};
108103

104+
export type WebChannelServices =
105+
| {
106+
sync: SyncEngines;
107+
}
108+
// For sync optional flows (currently Relay and AiMode)
109+
| {
110+
relay: {};
111+
}
112+
| {
113+
aimode: {};
114+
};
115+
109116
// ref: [FxAccounts.sys.mjs](https://searchfox.org/mozilla-central/rev/82828dba9e290914eddd294a0871533875b3a0b5/services/fxaccounts/FxAccounts.sys.mjs#910)
110117
export type FxALoginSignedInUserRequest = FxALoginRequest & {
111118
authAt: number;

packages/fxa-settings/src/lib/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ export const Constants = {
7575

7676
RELIER_DEFAULT_SERVICE_NAME: 'Account Settings',
7777
RELIER_SYNC_SERVICE_NAME: 'Firefox Sync',
78+
// Most of the time we rely on checking the `client_id` against the name we
79+
// have in our DB, but these services are for Firefox Client oauth native flows
80+
// flows that use the Firefox `client_id`. These may be placeholders/defaults
81+
// we move some of these into the CMS in FXA-12378, based on entrypoint.
82+
RELIER_FF_CLIENT_RELAY_SERVICE_NAME: 'Firefox Relay',
83+
RELIER_FF_CLIENT_AI_MODE_SERVICE_NAME: 'Firefox AI Mode',
84+
7885
RELIER_KEYS_LENGTH: 32,
7986
RELIER_KEYS_CONTEXT_INFO_PREFIX: 'identity.mozilla.com/picl/v1/oauth/',
8087

packages/fxa-settings/src/models/integrations/integration.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

55
import { MozServices } from '../../lib/types';
6+
import { WebChannelServices, SyncEngines } from '../../lib/channels/firefox';
67
import { IntegrationData } from './data/data';
78
import { IntegrationFeatures } from './features';
89
import {
@@ -69,10 +70,6 @@ export class GenericIntegration<
6970
this.features = { ...this.features, ...features } as TFeatures;
7071
}
7172

72-
hasWebChannelSupport() {
73-
return this.isSync() || this.isFirefoxClientServiceRelay();
74-
}
75-
7673
isSync() {
7774
return false;
7875
}
@@ -85,6 +82,18 @@ export class GenericIntegration<
8582
return false;
8683
}
8784

85+
isFirefoxClientServiceAiMode() {
86+
return false;
87+
}
88+
89+
// Practically, this will never be called unless the integration is
90+
// an oauth-native-integration, but provide a reasonable default.
91+
getWebChannelServices(
92+
_syncEngines?: SyncEngines
93+
): WebChannelServices | undefined {
94+
return undefined;
95+
}
96+
8897
isFirefoxMobileClient() {
8998
return false;
9099
}
@@ -166,6 +175,8 @@ export class GenericIntegration<
166175

167176
getCmsInfo() {
168177
// Still check for an empty object and only return if not empty.
169-
return Object.keys(this.cmsInfo || {}).length > 0 ? this.cmsInfo : undefined;
178+
return Object.keys(this.cmsInfo || {}).length > 0
179+
? this.cmsInfo
180+
: undefined;
170181
}
171182
}

packages/fxa-settings/src/models/integrations/oauth-native-integration.test.ts

Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ModelDataStore, GenericData } from '../../lib/model-data';
66
import {
77
OAuthNativeClients,
88
OAuthNativeIntegration,
9+
OAuthNativeServices,
910
} from './oauth-native-integration';
1011
import { OAuthWebIntegration } from './oauth-web-integration';
1112

@@ -27,7 +28,7 @@ describe('OAuthNativeIntegration', function () {
2728
beforeEach(function () {
2829
data = new GenericData({
2930
clientId: OAuthNativeClients.FirefoxIOS,
30-
service: 'sync',
31+
service: OAuthNativeServices.Sync,
3132
});
3233
oauthData = new GenericData({
3334
scope: 'profile',
@@ -38,7 +39,7 @@ describe('OAuthNativeIntegration', function () {
3839
isPromptNoneEnabled: true,
3940
isPromptNoneEnabledClientIds: [],
4041
});
41-
model.data.service = 'sync';
42+
model.data.service = OAuthNativeServices.Sync;
4243
model.data.state = 'aaaa';
4344
model.data.clientId = '123abc';
4445
});
@@ -51,7 +52,7 @@ describe('OAuthNativeIntegration', function () {
5152
describe('isSync', () => {
5253
it('returns true for Firefox desktop client when service is sync', () => {
5354
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
54-
model.data.service = 'sync';
55+
model.data.service = OAuthNativeServices.Sync;
5556
expect(model.isSync()).toBe(true);
5657
});
5758

@@ -77,21 +78,21 @@ describe('OAuthNativeIntegration', function () {
7778

7879
it('returns false for non-Sync services', () => {
7980
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
80-
model.data.service = 'relay';
81+
model.data.service = OAuthNativeServices.Relay;
8182
expect(model.isSync()).toBe(false);
8283
});
8384
});
8485

8586
describe('isDesktopSync', () => {
8687
it('returns true when client is Firefox desktop and service is sync', () => {
8788
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
88-
model.data.service = 'sync';
89+
model.data.service = OAuthNativeServices.Sync;
8990
expect(model.isDesktopSync()).toBe(true);
9091
});
9192

9293
it('returns false for non-sync service', () => {
9394
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
94-
model.data.service = 'relay';
95+
model.data.service = OAuthNativeServices.Relay;
9596
expect(model.isDesktopSync()).toBe(false);
9697
});
9798
});
@@ -135,15 +136,96 @@ describe('OAuthNativeIntegration', function () {
135136
});
136137
});
137138

138-
describe('serviceName', () => {
139+
describe('getServiceName', () => {
139140
it('returns "Firefox" for non-sync services', () => {
140141
model.data.service = 'non-sync-service';
141-
expect(model.serviceName).toBe('Firefox');
142+
expect(model.getServiceName()).toBe('Firefox');
142143
});
143144

144145
it('returns Sync service name for sync service', () => {
145-
model.data.service = 'sync';
146-
expect(model.serviceName).toBe('Firefox Sync');
146+
model.data.service = OAuthNativeServices.Sync;
147+
expect(model.getServiceName()).toBe('Firefox Sync');
148+
});
149+
150+
it('returns Relay service name for relay service', () => {
151+
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
152+
model.data.service = OAuthNativeServices.Relay;
153+
expect(model.getServiceName()).toBe('Firefox Relay');
154+
});
155+
156+
it('returns AI Mode service name for aimode service', () => {
157+
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
158+
model.data.service = OAuthNativeServices.AiMode;
159+
expect(model.getServiceName()).toBe('Firefox AI Mode');
160+
});
161+
});
162+
163+
describe('isFirefoxClientServiceRelay', () => {
164+
it('returns true when service is relay', () => {
165+
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
166+
model.data.service = OAuthNativeServices.Relay;
167+
expect(model.isFirefoxClientServiceRelay()).toBe(true);
168+
});
169+
170+
it('returns false when service is sync', () => {
171+
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
172+
model.data.service = OAuthNativeServices.Sync;
173+
expect(model.isFirefoxClientServiceRelay()).toBe(false);
174+
});
175+
176+
it('returns false when service is aimode', () => {
177+
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
178+
model.data.service = OAuthNativeServices.AiMode;
179+
expect(model.isFirefoxClientServiceRelay()).toBe(false);
180+
});
181+
});
182+
183+
describe('isFirefoxClientServiceAiMode', () => {
184+
it('returns true when service is aimode', () => {
185+
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
186+
model.data.service = OAuthNativeServices.AiMode;
187+
expect(model.isFirefoxClientServiceAiMode()).toBe(true);
188+
});
189+
190+
it('returns false when service is sync', () => {
191+
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
192+
model.data.service = OAuthNativeServices.Sync;
193+
expect(model.isFirefoxClientServiceAiMode()).toBe(false);
194+
});
195+
196+
it('returns false when service is relay', () => {
197+
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
198+
model.data.service = OAuthNativeServices.Relay;
199+
expect(model.isFirefoxClientServiceAiMode()).toBe(false);
200+
});
201+
});
202+
203+
describe('getWebChannelServices', () => {
204+
it('returns relay services when service is relay', () => {
205+
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
206+
model.data.service = OAuthNativeServices.Relay;
207+
expect(model.getWebChannelServices()).toEqual({ relay: {} });
208+
});
209+
210+
it('returns aimode services when service is aimode', () => {
211+
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
212+
model.data.service = OAuthNativeServices.AiMode;
213+
expect(model.getWebChannelServices()).toEqual({ aimode: {} });
214+
});
215+
216+
it('returns sync services when service is sync', () => {
217+
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
218+
model.data.service = OAuthNativeServices.Sync;
219+
const syncEngines = { offeredEngines: ['tabs'], declinedEngines: [] };
220+
expect(model.getWebChannelServices(syncEngines)).toEqual({
221+
sync: syncEngines,
222+
});
223+
});
224+
225+
it('returns sync services with empty object when no sync engines provided', () => {
226+
model.clientInfo = mockClientInfo(OAuthNativeClients.FirefoxDesktop);
227+
model.data.service = OAuthNativeServices.Sync;
228+
expect(model.getWebChannelServices()).toEqual({ sync: {} });
147229
});
148230
});
149231
});

packages/fxa-settings/src/models/integrations/oauth-native-integration.ts

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5+
import { SyncEngines } from '../../lib/channels/firefox';
56
import { Constants } from '../../lib/constants';
67
import { ModelDataStore } from '../../lib/model-data';
78
import { Integration, IntegrationType } from './integration';
@@ -47,6 +48,7 @@ export enum OAuthNativeClients {
4748
export enum OAuthNativeServices {
4849
Sync = 'sync',
4950
Relay = 'relay',
51+
AiMode = 'aimode',
5052
}
5153

5254
/**
@@ -79,19 +81,31 @@ export class OAuthNativeIntegration extends OAuthWebIntegration {
7981
}
8082

8183
isDesktopSync() {
84+
return this.isFirefoxDesktopClient() && this.isDefaultSyncService();
85+
}
86+
87+
private isFirefoxClient() {
88+
return this.isFirefoxDesktopClient() || this.isFirefoxMobileClient();
89+
}
90+
91+
// Sync should always provide a `service=sync` parameter for all Fx Desktop versions
92+
// and newer mobile versions. We'll default to Sync if it's missing.
93+
private isDefaultSyncService() {
8294
return (
83-
this.isFirefoxDesktopClient() &&
84-
// Sync oauth desktop should always provide a `service=sync` parameter but
85-
// we'll also default to Sync if it's missing.
86-
(this.data.service === undefined ||
87-
this.data.service === OAuthNativeServices.Sync)
95+
this.data.service === undefined ||
96+
this.data.service === OAuthNativeServices.Sync
8897
);
8998
}
9099

91100
isFirefoxClientServiceRelay() {
92101
return (
93-
(this.isFirefoxDesktopClient() || this.isFirefoxMobileClient()) &&
94-
this.data.service === OAuthNativeServices.Relay
102+
this.isFirefoxClient() && this.data.service === OAuthNativeServices.Relay
103+
);
104+
}
105+
106+
isFirefoxClientServiceAiMode() {
107+
return (
108+
this.isFirefoxClient() && this.data.service === OAuthNativeServices.AiMode
95109
);
96110
}
97111

@@ -113,15 +127,34 @@ export class OAuthNativeIntegration extends OAuthWebIntegration {
113127
}
114128

115129
wantsKeys() {
130+
// TODO: this will not always be true when working on FXA-12374
116131
return true;
117132
}
118133

119-
// TODO in FXA-10313, check for "Relay" or whatever makes sense at implementation
120-
get serviceName() {
121-
if (this.data.service === 'sync') {
134+
getWebChannelServices(syncEngines?: SyncEngines) {
135+
if (this.isFirefoxClientServiceRelay()) {
136+
return { relay: {} };
137+
}
138+
if (this.isFirefoxClientServiceAiMode()) {
139+
return { aimode: {} };
140+
}
141+
if (this.isDefaultSyncService()) {
142+
return { sync: syncEngines || {} };
143+
}
144+
return undefined;
145+
}
146+
147+
getServiceName() {
148+
if (this.isDefaultSyncService()) {
122149
return Constants.RELIER_SYNC_SERVICE_NAME;
123-
} else {
124-
return 'Firefox';
125150
}
151+
if (this.isFirefoxClientServiceRelay()) {
152+
return Constants.RELIER_FF_CLIENT_RELAY_SERVICE_NAME;
153+
}
154+
if (this.isFirefoxClientServiceAiMode()) {
155+
return Constants.RELIER_FF_CLIENT_AI_MODE_SERVICE_NAME;
156+
}
157+
// TODO: handle Thunderbird case better? FXA-10848
158+
return 'Firefox';
126159
}
127160
}

packages/fxa-settings/src/pages/PostVerify/SetPassword/container.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
MOCK_STORED_ACCOUNT,
1818
MOCK_UID,
1919
MOCK_UNWRAP_BKEY,
20+
mockGetWebChannelServices,
2021
} from '../../mocks';
2122
import { SetPasswordProps } from './interfaces';
2223
import { LocationProvider } from '@reach/router';
@@ -147,8 +148,10 @@ function mockSyncDesktopV3Integration() {
147148
data: { service: 'sync' },
148149
isDesktopSync: () => true,
149150
isFirefoxClientServiceRelay: () => false,
151+
isFirefoxClientServiceAiMode: () => false,
150152
isFirefoxMobileClient: () => false,
151153
getCmsInfo: () => undefined,
154+
getWebChannelServices: mockGetWebChannelServices({ isSync: true }),
152155
} as ModelsModule.Integration;
153156
}
154157
function mockOAuthNativeIntegration(
@@ -165,8 +168,10 @@ function mockOAuthNativeIntegration(
165168
data: { service: 'sync' },
166169
isDesktopSync: () => true,
167170
isFirefoxClientServiceRelay: () => false,
171+
isFirefoxClientServiceAiMode: () => false,
168172
isFirefoxMobileClient: () => isFirefoxMobileClient,
169173
getCmsInfo: () => undefined,
174+
getWebChannelServices: mockGetWebChannelServices({ isSync: true }),
170175
} as ModelsModule.Integration;
171176
}
172177

packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/container.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,7 @@ const CompleteResetPasswordContainer = ({
239239
keyFetchToken: accountResetData.keyFetchToken,
240240
unwrapBKey: accountResetData.unwrapBKey,
241241
}),
242-
services: integration.isFirefoxClientServiceRelay()
243-
? { relay: {} }
244-
: { sync: {} },
242+
services: integration.getWebChannelServices(),
245243
});
246244

247245
if (isOAuth) {

0 commit comments

Comments
 (0)