Skip to content

Implement the shared setup-flow mechanism + the return to the hub #13338

Closed

Description

@techanvil

Feature Description

Most of the features the hub lists are set up in a flow Site Kit already has: the user leaves the hub for the service's own setup screens, and often for Google's, and comes back when it's done. The hub adds no setup UI of its own for these — clicking its call to action should start exactly the flow the user would get from Settings' Connect more services tab today.

Two things make that more than a link. The first is the handoff itself: the service has to be activated before the user can be sent into its setup. The second is the trip back. Every one of these flows finishes by sending the user to the dashboard, which is the right place when they started there — but a user who came from the hub to set up one feature has more they want to do, and dropping them on the dashboard makes them find their way back. So the hub records that it launched the setup, and a flow that finds such a record completes to the hub instead, on the tab the user started from.

This issue builds that shared path — the handoff, the record, and the return for every module that completes through Site Kit's common module-setup completion. It covers the features that are a plain service activation: Analytics, Ads, AdSense, PageSpeed Insights, Sign in with Google, and Reader Revenue Manager's full setup. The flows that finish somewhere of their own, and so have to consult the record themselves, are covered by #13340; the Reader Revenue Manager newsletter feature and Key Metrics have their own issues, #13345 and #13348. An activation that fails before the user leaves the hub is reported by #13339.

Everywhere else that starts one of these setups — a dashboard banner, Settings, anywhere that isn't the hub — is unaffected: the hub is an override, never a new default.

For reference, see the setup-flow — navigate away to a Site Kit setup flow and Returning to the hub sections in the design doc.


Do not alter or remove anything below. The following sections will be managed by moderators only.

Acceptance criteria

  • Clicking a feature's call to action (Build FeatureCTA with gating rules #13322) where that feature is set up in an existing setup flow activates the feature's Google service and takes the user into that service's setup, exactly as Settings' Connect more services tab does today — for Analytics, Ads, AdSense, Sign in with Google, and Reader Revenue Manager (full setup).
  • PageSpeed Insights is activated immediately, with nothing to configure, so the user stays on the hub. The confirmation they see is covered by Implement in-place success detection & notice #13346.
  • Before the user is taken away, the hub records which feature they set out to set up and which tab they were on. The record belongs to the browser tab they left from, survives signing in to Google, and expires after an hour.
  • Completing the setup returns the user to the hub, on the tab they started from, instead of the dashboard it goes to today. This covers every module that finishes through Site Kit's shared module-setup completion: AdSense, Sign in with Google, Reader Revenue Manager, and Ads where it doesn't use the PAX flow.
  • Otherwise every flow keeps the destination it has today: where there's no record, where it's expired, or where it belongs to some other setup the user left unfinished rather than the one that just completed, the flow completes exactly where it does now.
  • Landing back on the hub uses the record up, so the user is routed once: reloading the hub, or coming back to it later, doesn't route them again. A user who abandoned the setup and opens the hub afterwards simply gets it in its normal state.
  • Where the service can't be activated, so the user never leaves the hub, the failure is reported on the feature's card — covered by Implement the shared setup-failure notice #13339.
  • Where the browser has no storage for the record, setup still works: the feature is set up and the user completes to the dashboard as today.
  • The flows that finish on a screen of their own — Analytics' Key Metrics step, the Ads PAX flow, the Ad Blocking Recovery screen, the Key Metrics questionnaire, and Reader Revenue Manager's express route — are untouched here, and covered by Return the user to the hub from bespoke completion handlers #13340 and their own features' issues, Implement setup for the Key Metrics feature #13348 and Implement setup for the RRM newsletter sign-up feature #13345.
  • The success confirmation shown on return is covered by Implement the top-of-list success notice on return #13356: this issue lands the user on the right tab and shows nothing there.

Implementation Brief

  • In assets/js/googlesitekit/feature-discovery/, create pending-setup.ts:

    • Define a cache key constant, FEATURE_DISCOVERY_SETUP_CACHE_KEY = 'feature_discovery_setup' — the hub's hash-route segments (the tab components themselves land in #13321/#13328).
    • Export getCurrentFeatureDiscoveryTab(): parses global.location.hash (#/whats-new, #/all-services) and falls back to All Services tab for anything else.
    • Export setPendingSetup( featureSlug: string, returnTab: string ), wrapping setItem( FEATURE_DISCOVERY_SETUP_CACHE_KEY, { featureSlug, returnTab } ) from @/js/googlesitekit/api/cache. Use the helper's default TTL (HOUR_IN_SECONDS) rather than passing one.
    • Export consumePendingSetup(): Promise<{ featureSlug: string; returnTab: string } | null>: reads via getItem() then deletes via deleteItem(), returning value on a cache hit or null otherwise. This read-and-delete is what makes sure the record can only ever be consumed once — called from exactly one place, FeatureDiscoveryApp's mount effect described below.
    • Export getPendingSetupReturnURL( select: Select, featureSlug: string ): Promise<string | undefined> — read the record via getItem() only (no deleteItem(), so it's left in place for consumePendingSetup() to pick up once the hub actually mounts), and return the plain hub admin URL, select( CORE_SITE ).getAdminURL( 'googlesitekit-features' ), only where record.featureSlug === featureSlug; otherwise return undefined. Matching directly on the feature slug — rather than resolving it through the catalog — is what lets this be called with whatever feature slug the caller already has in hand, so #13340's bespoke completion handlers can use it as-is. Stopping a stale record left by some other, abandoned hub setup from hijacking this one falls out of the same comparison. The URL carries no tab hash — the tab is restored from the consumed record once FeatureDiscoveryApp mounts.
    • Model this on the existing module_setup cache item — written by assets/js/components/settings/SetupModule.js and cleared by assets/js/components/setup/hooks/useFinishSetup.ts — but keep it in its own file/key since it carries a feature slug and a tab, not a module slug.
  • In assets/js/googlesitekit/datastore/feature-discovery/features.ts, implement the setupFeature( slug ) generator action (started as a placeholder in #13322):

    • Resolve the feature via getFeature( slug ) and return {} unless feature.setup.type === FEATURE_SETUP_TYPES.SETUP_FLOW — the background-toggle and in-place-panel types are built by their own issues.
    • Where feature.setup.moduleSlug is set (Analytics, Ads, AdSense, PageSpeed Insights, Sign in with Google, Reader Revenue Manager full setup):
      • Dispatch CORE_MODULES activateModule( moduleSlug ).
      • On { error }, return it as-is and stop — surfacing it on the card is #13339's job, out of scope here.
      • On success, read select( CORE_MODULES ).getModule( moduleSlug )?.SetupComponent. If it's falsy (e.g. PageSpeed Insights — no setup screen registered), then stop: the module is already connected, the user stays on the hub, and nothing is written or navigated to.
      • Otherwise, call setPendingSetup( slug, getCurrentFeatureDiscoveryTabPath() ), then dispatch CORE_LOCATION navigateTo( response.moduleReauthURL ). Note that the pending-setup cache item should be set before navigating to moduleReauthURL, — this is essential for the record to survive an OAuth round trip.
    • Import CORE_MODULES from @/js/googlesitekit/modules/datastore/constants and CORE_LOCATION from @/js/googlesitekit/datastore/location/constants; use the commonActions.getRegistry() / commonActions.await() pattern already used throughout the datastores (e.g. assets/js/googlesitekit/modules/datastore/modules.js).
    • Add the session-only state the hub reads once it mounts.
      • A receivePendingSetup( pendingSetup: { featureSlug: string; returnTab: string } | null ) action and reducer case storing it under state.pendingSetup (initial state null), plus a getPendingSetup( state ) selector returning it.
  • In assets/js/components/feature-discovery/FeatureDiscoveryApp.tsx, once on mount:

    • Call consumePendingSetup() and dispatch its result (a record or null) to receivePendingSetup() — this is the single place the record is ever consumed, so reloading or revisiting the hub afterwards can't route the user again.
  • In assets/js/components/feature-discovery/FeatureDiscoveryContent.tsx:

    • Read select( CORE_FEATURE_DISCOVERY ).getPendingSetup(). Where it names a returnTab, use that in place of the existing new-vs-returning-user calculation when computing defaultPath, so the component's existing <Redirect to={ defaultPath } /> fallback carries the user to the tab they set out from.
  • In assets/js/components/feature-discovery/constants.ts

    • Add a small moduleSlug → feature slug lookup, covering just the plain module-activation features this issue handles: MODULE_SLUG_ANALYTICS_4 → 'analytics', MODULE_SLUG_ADS → 'ads', MODULE_SLUG_ADSENSE → 'adsense', MODULE_SLUG_PAGESPEED_INSIGHTS → 'pagespeed-insights', MODULE_SLUG_SIGN_IN_WITH_GOOGLE → 'sign-in-with-google', MODULE_SLUG_READER_REVENUE_MANAGER → 'reader-revenue-manager'
    • As per this discussion, add the descriptive comment in the code that this mapping is a deliberate choice.
  • In assets/js/components/setup/hooks/useFinishSetup.ts:

    • In the branch that runs when finishSetup() is called with no redirectURL, before building the dashboard adminURL, look up this hook's moduleSlug in above mapping table and, where it maps to a feature slug, call await getPendingSetupReturnURL( registry.select, featureSlug ).
    • Where it resolves a URL, navigateTo() it and return, skipping the dashboard adminURL build.
    • Where it resolves undefined — no record, an expired one, or one belonging to a different feature — or where moduleSlug isn't in the lookup, fall through to today's dashboard destination unchanged.
    • Where browser storage isn't available, getItem()'s existing cache-miss behaviour means getPendingSetupReturnURL() resolves undefined the same way, so setup still completes — to the dashboard.

Test Coverage

  • Add unit tests for pending-setup.ts: setPendingSetup/consumePendingSetup round-tripping (including the read-and-delete/single-consumption behaviour), and getPendingSetupReturnURL() matching, not matching, and finding no record — confirming it never deletes the item it reads.
  • Add/extend features.test.ts for setupFeature() (the moduleSlug activation success, error, and no-SetupComponent paths) and for receivePendingSetup()/getPendingSetup().
  • Add unit tests for the new mount hook, confirming consumePendingSetup() is called exactly once per mount and its result reaches getPendingSetup().
  • Extend FeatureDiscoveryContent's test coverage to confirm it redirects to the consumed record's returnTab in place of the computed default when one is present.
  • Extend useFinishSetup.test.ts to cover: the moduleSlug → feature slug lookup for each of the six mapped modules, returning to the hub on a matching pending-setup record, falling through to the dashboard for an unmapped moduleSlug or a mismatched/absent/expired record, and confirming the existing redirectURL branch is unaffected.

QA Brief

  • Prerequisites: the featureDiscoveryHub feature flag is enabled (Tester Settings → General → Enabled feature flags), Site Kit is connected, and at least one of AdSense or Sign in with Google is not yet set up. Use whichever you can complete: AdSense needs an AdSense account on the connected Google account, and Sign in with Google needs a Client ID (it opens Google in a new tab to create one; paste the ID back into the Site Kit tab).

  • Go to Site Kit → Add Features and open the All services and features tab.

  • Click the CTA for a feature that isn't set up yet (for example Set up AdSense). Verify you're taken to that module's setup screen, and that its URL is …page=googlesitekit-dashboard&slug=adsense&reAuth=true.

  • Complete the setup. Verify you land back on the hub on the All services and features tab (URL ends #/all-services), not on the dashboard.

  • Reload the hub, and verify nothing redirects you again and you stay where you are.

  • Repeat from the What's new? tab: set Tester Settings → General → "Force initial Site Kit plugin version" to something old (for example 1.150.0) so features show as new, click the CTA from that tab, and complete the setup. Verify you return to the What's new? tab. Clear the forced version afterwards.

  • If you can use an account with a publication, repeat with Set up Reader Revenue Manager (full setup). Verify the same return to the hub.

  • With PageSpeed Insights disconnected (Settings → Connected Services), click Set up PageSpeed Insights in the hub. Verify it's activated immediately, with nothing to configure, and you stay on the hub.

  • Verify setups started elsewhere still complete to the dashboard as before: Settings → Connect More Services → set up a module → complete → you land on the dashboard.

  • Verify the record belongs to the browser tab: start a setup from the hub, copy the setup screen's URL into a new tab, and complete the setup there. Verify you land on the dashboard.

  • Verify the record expires: start a setup from the hub, and before completing it run the snippet below in the console, then complete the setup. Verify you land on the dashboard.

    const key = Object.keys( sessionStorage ).find( ( k ) => k.includes( 'feature_discovery_setup' ) ); const value = JSON.parse( sessionStorage[ key ] ); value.timestamp -= 3700; sessionStorage[ key ] = JSON.stringify( value );
  • The next two steps simulate damaged browser storage, which shouldn't happen in normal use. Verify a corrupted record doesn't stop the hub from loading: start a setup from the hub so you're on the module's setup screen, then run the snippet below in the console. Then go to Site Kit → Add Features. Verify the hub loads normally, on its usual default tab, with no blank area.

  • Verify a corrupted record doesn't stop a setup from completing: start a setup from the hub again, and on the setup screen run the same snippet. Then complete the setup. Verify you land on the dashboard, not on a blank page and not stuck on the setup screen.

    const key = Object.keys( sessionStorage ).find( ( k ) => k.includes( 'feature_discovery_setup' ) ); sessionStorage[ key ] = '{';
  • If you can, verify the Google sign-in round trip: disconnect the module, then disconnect and reconnect your Google account in Site Kit (profile menu → Disconnect) so the module isn't active when you reconnect. Click the module's CTA in the hub. Verify you go to Google's permissions screen in the same tab, are returned to the module's setup screen, and on completing it land back on the hub on the tab you started from.

Notes for the tester

Changelog entry

  • Enable setting up the Analytics, Ads, AdSense, Sign in with Google, and Reader Revenue Manager features on the Feature Discovery Hub.

Activity

  1. self-assigned this
    on Aug 21, 2026
  2. self-assigned this
    on Sep 16, 2026
  3. techanvil commented on Sep 24, 2026

    @techanvil
    CollaboratorAuthor

    Hi @ankitrox, thanks for drafting this IB. A few points:

    • We've already got FEATURE_DISCOVERY_TABS and DEFAULT_TAB_PATH in feature-discovery/constants.ts, I think we can use these instead of defining a new constant.
    • It might be a bit unclear from the AC for Return the user to the hub from bespoke completion handlers #13340 but the intention was for the getSetupURL() handling to be implemented in that issue, as the related features are out of scope here. As you've written the IB for that issue I've assigned it back to you - please move this aspect to its IB.
    • It's worth noting we should set the module_setup cache item before navigating to moduleReauthURL, as we do elsewhere when setting up a module (needed for the setup to survive an OAuth retry).
  4. removed their assignment
    on Sep 24, 2026
  5. 22 remaining items

  6. removed their assignment
    on Sep 30, 2026
  7. self-assigned this
    on Oct 9, 2026
  8. kelvinballoo commented on Oct 12, 2026

    @kelvinballoo
    Collaborator

    QA Update ⚠️

    Tested this and I have a couple of feedback:

    ITEM 1:
    Whenever, I set up a module:

    • There is no 'Spinning' icon on the setup CTA. Usually we should have one.
    • After the setup is complete, I return back to the hub page correctly. However, there is no success message indicating the module was successfully set up. It should have a success message in my opinion. Is this catered in a separate ticket?

    ITEM 2:

    For the PSI module, I set it up from the hub.
    As the QAB states, it isactivated immediately, with nothing to configure, and you stay on the hub.
    However, there is no success message and the PSI feature remains intact in the list of features, even after successfully set up. By right, it should disappear.
    The UX isn't good here. The user might not know if PSI has been activated.

    Details
    13338.-.04.-.PSI.-.ok.but.no.msg.and.the.PSI.banner.remains.NOT.ok.mov
  9. ncoetzer commented on Oct 13, 2026

    @ncoetzer
    Collaborator

    @kelvinballoo Thanks for the review and feedback.

    ITEM 1

    There is no 'Spinning' icon on the setup CTA. Usually we should have one.

    The spinner is there, but it's white against a white card background, so you can't see it. The spinner was introduced in #13322, but we'll need to account for lighter backgrounds. I'll raise a separate bug issue for that.

    After the setup is complete, I return back to the hub page correctly. However, there is no success message indicating the module was successfully set up. It should have a success message in my opinion. Is this catered in a separate ticket?

    I believe this will be taken care of in #13356

    ITEM 2: This will be handled in #13346. Also mentioned in the AC above 🙂

  10. removed their assignment
    on Oct 13, 2026
  11. kelvinballoo commented on Oct 14, 2026

    @kelvinballoo
    Collaborator

    QA Update ✅

    Thanks for clarifying @ncoetzer
    We should be good moving this to approval.

    Tested as follows:

    • Adsense setup from the hub was fine and we are redirected to the hub after the setup. Reloading the page at the hub maintained the same page.

      Details

      https://github.com/user-attachments/assets/4cff9364-3406-41fa-ab89-9f35564c7a09
      https://github.com/user-attachments/assets/05361167-fda7-4d5c-bcb7-122120bebdd7

    • Modules starting from a specific hub page will reroute back to the same hub page after setup Tested with RRM and Ads and SiwG.

      Details
      13338.-.02.-.Ads.setup.from.what.s.new.mov
      13338.-.03.-.RRM.setup.ok.mov
      13338.-.09.-SiwG.ok.mov
      13338.-.09.-SiwG.ok.2.mov
    • PSI is activated immediately and user remains on the same page.

      Details
      13338.-.04.-.PSI.-.ok.but.no.msg.and.the.PSI.banner.remains.NOT.ok.mov
    • Modules that are set up from the connect services page will route to the main dashboard after setup.

      Details
      13338.-.05.-.module.setup.from.settings.ok.mov
    • Tested a scenario where we copy out the link to a new tab. After setup, we are redirected to the main dashboard as expected.

      Details
      13338.-.06.-.RRM.-.start.hub.copy.new.tab.return.to.dashboard.ok.mov
    • Tested the first and 2nd snippet and in both cases, we are redirected to the main dashboard after the setup, as expected.

      Details
      13338.-.07.-.First.snippet.return.to.tdesktop.ok.mov
      13338.-.08.-second.snippet.mov
  12. removed their assignment
    on Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0High priorityTeam MIssues for Squad 2Type: EnhancementImprovement of an existing feature

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions