11import BrowserDetect from "./browserdetect" ;
2+ import UITour from "./libs/uitour" ;
23
34/**
45 * This class is responsible for managing the state for a wizard that
@@ -11,6 +12,11 @@ import BrowserDetect from "./browserdetect";
1112export default class SwitchingDevicesWizardManager {
1213 #formWizard = null ;
1314
15+ // For versions of Firefox < 114, we use this value as a polling interval
16+ // to check for FxA sign-in state changes.
17+ #pollIntervalMs = 500 ;
18+ #pollIntervalID = null ;
19+
1420 // Some of these #state properties use snake_case since there are some
1521 // properties that are going to be sent as queryParameters, and this
1622 // lets us not worry about translating them from camelCase to snake_case.
@@ -70,6 +76,8 @@ export default class SwitchingDevicesWizardManager {
7076 steps = Object . freeze ( [
7177 {
7278 name : "sign-into-fxa" ,
79+ status : "active" ,
80+ label : gettext ( "Create an account" ) ,
7381 exitConditionsMet ( state ) {
7482 return state . fxaSignedIn ;
7583 } ,
@@ -92,6 +100,8 @@ export default class SwitchingDevicesWizardManager {
92100 } ,
93101 {
94102 name : "configure-sync" ,
103+ status : "unavailable" ,
104+ label : gettext ( "Sync your data" ) ,
95105 exitConditionsMet ( state ) {
96106 return state . syncEnabled && state . confirmedSyncChoices ;
97107 } ,
@@ -103,6 +113,8 @@ export default class SwitchingDevicesWizardManager {
103113 } ,
104114 {
105115 name : "setup-new-device" ,
116+ status : "unavailable" ,
117+ label : gettext ( "Set up your new device" ) ,
106118 exitConditionsMet ( state ) {
107119 return false ;
108120 } ,
@@ -123,8 +135,10 @@ export default class SwitchingDevicesWizardManager {
123135 * An optional fake object to pass to the BrowserDetect library to simulate
124136 * what is returned from a WebChannel on a Firefox Desktop browser for
125137 * a request for troubleshooting data.
138+ * @param {number } [pollIntervalMs=undefined]
139+ * The interval for polling for FxA state changes while under test.
126140 */
127- constructor ( formWizard , fakeUA , fakeTroubleshooting ) {
141+ constructor ( formWizard , fakeUA , fakeTroubleshooting , pollIntervalMs ) {
128142 this . #formWizard = formWizard ;
129143
130144 if ( ! this . #formWizard. hasAttribute ( "fxa-root" ) ) {
@@ -134,6 +148,8 @@ export default class SwitchingDevicesWizardManager {
134148 ) ;
135149 }
136150
151+ this . #setupFormWizardStepIndicator( ) ;
152+
137153 this . #state. fxaRoot = this . #formWizard. getAttribute ( "fxa-root" ) ;
138154
139155 // If the page was loaded with any UTM or entrypoint parameters, we'll want
@@ -155,7 +171,7 @@ export default class SwitchingDevicesWizardManager {
155171 }
156172 }
157173
158- this . #init( fakeUA , fakeTroubleshooting ) ;
174+ this . #init( fakeUA , fakeTroubleshooting , pollIntervalMs ) ;
159175 }
160176
161177 /**
@@ -170,20 +186,44 @@ export default class SwitchingDevicesWizardManager {
170186 * An optional fake object to pass to the BrowserDetect library to simulate
171187 * what is returned from a WebChannel on a Firefox Desktop browser for
172188 * a request for troubleshooting data.
189+ * @param {number } [pollIntervalMs=undefined]
190+ * The interval for polling for FxA state changes while under test.
173191 */
174- async #init( fakeUA , fakeTroubleshooting ) {
192+ async #init( fakeUA , fakeTroubleshooting , pollIntervalMs = this . #pollIntervalMs ) {
175193 try {
176194 let detect = new BrowserDetect ( fakeUA , null , fakeTroubleshooting ) ;
177195 let browser = await detect . getBrowser ( ) ;
178196 let platform = await detect . getOS ( ) ;
179197 if ( browser . mozilla && ! platform . mobile ) {
198+ await new Promise ( resolve => {
199+ UITour . ping ( resolve ) ;
200+ } ) ;
201+
202+ await this . #updateFxAState( ) ;
203+
204+ if ( browser . version . major >= 114 ) {
205+ // Firefox 114+ allows us to get notified when the FxA sign-in state
206+ // changes through UITour.
207+ await new Promise ( resolve => {
208+ UITour . observe ( ( name , params ) => {
209+ this . #onUITourNotification( name , params ) ;
210+ } , resolve ) ;
211+ } ) ;
212+ } else {
213+ // For older Firefox versions, we'll do polling.
214+ this . #pollIntervalID = window . setInterval ( ( ) => {
215+ this . #updateFxAState( ) ;
216+ } , pollIntervalMs ) ;
217+ }
218+
180219 // We need to get some query parameters from the FxA server before we
181220 // show the user any kind of form to create or sign-in to an account.
182221 // See https://mozilla.github.io/ecosystem-platform/relying-parties/reference/metrics-for-relying-parties#relying-party-hosted-email-form.
183222 await this . #requestMetricsParams( ) ;
184223 return ;
185224 }
186225 } catch ( e ) {
226+ console . error ( e ) ;
187227 // Intentional fall-through - we want to do this if any part of the
188228 // UA computation didn't meet our criteria OR failed.
189229 }
@@ -198,6 +238,15 @@ export default class SwitchingDevicesWizardManager {
198238 ) ;
199239 }
200240
241+ /**
242+ * A testing-only function to do any cleanup once an instance of
243+ * SwitchingDevicesWizardManager is being thrown away.
244+ */
245+ destroy ( ) {
246+ window . clearInterval ( this . #pollIntervalID) ;
247+ this . #pollIntervalID = null ;
248+ }
249+
201250 /**
202251 * Returns a fresh copy of the internal state object. This is mainly
203252 * for testing.
@@ -218,8 +267,16 @@ export default class SwitchingDevicesWizardManager {
218267 * mechanism as when using `Object.assign`.
219268 */
220269 #updateState( stateDiff ) {
221- this . #state = Object . assign ( this . #state, stateDiff ) ;
222- this . #recomputeCurrentStep( ) ;
270+ let oldState = this . state ;
271+ let newState = Object . assign ( this . #state, stateDiff ) ;
272+ this . #state = newState ;
273+
274+ for ( let prop in oldState ) {
275+ if ( oldState [ prop ] !== newState [ prop ] ) {
276+ this . #recomputeCurrentStep( ) ;
277+ break ;
278+ }
279+ }
223280 }
224281
225282 /**
@@ -238,6 +295,70 @@ export default class SwitchingDevicesWizardManager {
238295 }
239296 }
240297
298+ /**
299+ * Sets up the step indicator on the #formWizard based on the
300+ * default state of this.steps. This should be called either
301+ * during initialization or if the whole wizard is being reset
302+ * back to the starting state.
303+ */
304+ #setupFormWizardStepIndicator( ) {
305+ let fwSteps = this . steps . map ( ( { name, status, label } ) => {
306+ return { name, status, label } ;
307+ } ) ;
308+ this . #formWizard. steps = fwSteps ;
309+ }
310+
311+ /**
312+ * Handler for UITour notifications. Specifically, this monitors
313+ * for changes to the FxA sign-in state.
314+ *
315+ * @param {string } name
316+ * The name of the UITour notification.
317+ * @param {object } params
318+ * Extra parameters that UITour sends with each notification.
319+ * @return {Promise<undefined> }
320+ * Resolves after recomputing the FxA sign-in state.
321+ */
322+ async #onUITourNotification( name , params ) {
323+ if ( name == "FxA:SignedInStateChange" ) {
324+ await this . #updateFxAState( ) ;
325+ }
326+ }
327+
328+ /**
329+ * Uses UITour to query for the current FxA sign-in state. A user
330+ * is considered signed in if their FxA account is both setup and
331+ * the account state is "ok" (as in, they have verified their email
332+ * address).
333+ *
334+ * Once the sign-in state is determined from UITour, the internal
335+ * state of the SwichingDevicesWizardManager is updated to reflect
336+ * it.
337+ *
338+ * In the event that the state is transitioning from "signed in" to
339+ * "signed out", the #formWizard is reset back to its starting point.
340+ */
341+ async #updateFxAState( ) {
342+ let fxaConfig = await new Promise ( resolve => {
343+ UITour . getConfiguration ( "fxa" , resolve ) ;
344+ } ) ;
345+
346+ if ( fxaConfig . setup && fxaConfig . accountStateOK ) {
347+ this . #updateState( {
348+ fxaSignedIn : true ,
349+ syncEnabled : ! ! fxaConfig . browserServices ?. sync ?. setup ,
350+ } ) ;
351+ } else if ( this . #state. fxaSignedIn ) {
352+ // If we've gone from being signed in to signed out, we need to
353+ // reset our state.
354+ this . #setupFormWizardStepIndicator( ) ;
355+ this . #updateState( {
356+ fxaSignedIn : false ,
357+ syncEnabled : false ,
358+ } ) ;
359+ }
360+ }
361+
241362 /**
242363 * Makes contact with the FxA endpoint pointed at by state.fxaRoot/metrics-flow to
243364 * get the flow_id and flow_begin_time parameters. Once those parameters are
@@ -258,18 +379,26 @@ export default class SwitchingDevicesWizardManager {
258379 params . set ( paramName , this . #state[ paramName ] ) ;
259380 }
260381 }
261- let response = await window . fetch (
262- `${ this . #state. fxaRoot } /metrics-flow?${ params } `
263- ) ;
382+ try {
383+ let response = await window . fetch (
384+ `${ this . #state. fxaRoot } /metrics-flow?${ params } `
385+ ) ;
264386
265- if ( response . status == 200 ) {
266- let { flowId, flowBeginTime } = await response . json ( ) ;
387+ if ( response . status == 200 ) {
388+ let { flowId, flowBeginTime } = await response . json ( ) ;
267389
268- let stateDiff = {
269- flow_id : flowId ,
270- flow_begin_time : flowBeginTime ,
271- } ;
272- this . #updateState( stateDiff ) ;
390+ let stateDiff = {
391+ flow_id : flowId ,
392+ flow_begin_time : flowBeginTime ,
393+ } ;
394+ this . #updateState( stateDiff ) ;
395+ }
396+ } catch ( e ) {
397+ // Log but intentionally ignore this case. If getting the metrics flow
398+ // parameters somehow failed (say, for example, a problem on the FxA
399+ // handler for metrics-flow), this shouldn't prevent the user from
400+ // completing their task.
401+ console . error ( e ) ;
273402 }
274403 }
275404}
0 commit comments