Repository: FlowFuse/flowfuse
Pull request #5269: extract tours into a separate module
Author: login: cstns
Created: 2025-03-07T14:27:01Z | Merged: 2025-03-25T12:42:17Z

## Prerequisites

based on https://github.com/FlowFuse/flowfuse/pull/5251

## Description

Extract ux tours into separate module

## Related Issue(s)

n/a

## Checklist

<!-- https://flowfuse.com/handbook/development/#defining-done -->

 - [x] I have read the [contribution guidelines](https://github.com/FlowFuse/flowfuse/blob/main/CONTRIBUTING.md)
 - [ ] Suitable unit/system level tests have been added and they pass <!-- If not adding test coverage, please clarify why not? -->
 - [ ] Documentation has been updated
    - [ ] Upgrade instructions
    - [ ] Configuration details
    - [ ] Concepts
 - [ ] Changes `flowforge.yml`?
    - [ ] Issue/PR raised on `FlowFuse/helm` to update ConfigMap Template
    - [ ] Issue/PR raised on `FlowFuse/CloudProject` to update values for Staging/Production

## Labels

 - [ ] Includes a DB migration? -> add the `area:migration` label




Diff:
diff --git a/frontend/src/components/DevicesBrowser.vue b/frontend/src/components/DevicesBrowser.vue
index f040a7305e..2cdaf07aa5 100644
--- a/frontend/src/components/DevicesBrowser.vue
+++ b/frontend/src/components/DevicesBrowser.vue
@@ -235,7 +235,7 @@
         @device-updated="deviceUpdated"
     >
         <template #description>
-            <p v-if="!featuresCheck?.isHostedInstancesEnabledForTeam && tours['first-device']">
+            <p v-if="!featuresCheck?.isHostedInstancesEnabledForTeam && tours.firstDevice">
                 Describe your new Remote Instance here, e.g. "Raspberry Pi", "Allen-Bradley PLC", etc.
             </p>
             <p v-else>
@@ -405,7 +405,7 @@ export default {
     },
     computed: {
         ...mapState('account', ['team', 'teamMembership']),
-        ...mapState('ux', ['tours']),
+        ...mapState('ux/tours', ['tours']),
         ...mapGetters('account', ['featuresCheck']),
         columns () {
             const columns = [
diff --git a/frontend/src/components/PageHeader.vue b/frontend/src/components/PageHeader.vue
index 4a7f648337..4e3f4e1609 100644
--- a/frontend/src/components/PageHeader.vue
+++ b/frontend/src/components/PageHeader.vue
@@ -181,22 +181,18 @@ export default {
         }
     },
     methods: {
-        ...mapActions('ux', ['toggleLeftDrawer', 'activateTour']),
+        ...mapActions('ux', ['toggleLeftDrawer']),
         openEducationModal () {
-            this.activateTour('education')
-            product.capture('clicked-open-education-modal')
+            this.$store.dispatch('ux/tours/openModal', 'education')
+                .then(() => product.capture('clicked-open-education-modal'))
+                .catch(e => e)
         },
         startWelcomeTour () {
-            return this.$store.dispatch('ux/resetTours')
+            return this.$store.dispatch('ux/tours/resetTours')
                 // it's unfortunate that we can't redirect premium users straight to the application device page, but we
                 // don't have available applications at this moment in time so they'll get redirected twice
                 .then(() => this.$router.push({ name: 'Applications' }))
-                .then(() => {
-                    // breathing room for the page, instances and devices to load for the tour to work properly
-                    setTimeout(() => {
-                        this.$store.dispatch('ux/activateTour', 'welcome')
-                    }, 1000)
-                })
+                .then(() => this.$store.dispatch('ux/tours/presentTour'))
         }
     }
 }
diff --git a/frontend/src/components/dialogs/EducationModal.vue b/frontend/src/components/dialogs/EducationModal.vue
index 04d710af7b..e9b36f8e91 100644
--- a/frontend/src/components/dialogs/EducationModal.vue
+++ b/frontend/src/components/dialogs/EducationModal.vue
@@ -49,12 +49,13 @@ export default {
     data () {
         return {
             isClosing: false,
-            closingTimer: 0
+            closingTimer: 0,
+            closingIntervalId: null
         }
     },
     computed: {
         ...mapGetters({
-            isOpen: 'ux/shouldShowEducationModal'
+            isOpen: 'ux/tours/shouldShowEducationModal'
         }),
         helpOptions () {
             return [
@@ -79,28 +80,31 @@ export default {
         }
     },
     methods: {
-        ...mapActions('ux', ['deactivateTour']),
+        ...mapActions('ux/tours', ['deactivateTour']),
         triggerClose () {
             if (this.isClosing) {
                 this.closeModal()
+                clearInterval(this.closingIntervalId)
+                this.closingTimer = 0
             } else {
                 this.isClosing = true
-
-                const intervalId = setInterval(() => {
+                this.closingIntervalId = setInterval(() => {
                     this.closingTimer += 0.225
 
                     if (this.closingTimer >= 100) {
-                        clearInterval(intervalId)
+                        clearInterval(this.closingIntervalId)
+                        this.closeModal()
                     }
                 }, 10)
-
-                setTimeout(() => this.closeModal(), 5000)
             }
         },
         closeModal () {
-            this.deactivateTour('education')
-            this.isClosing = false
-            this.closingTimer = 0
+            this.$store.dispatch('ux/tours/closeModal', 'education')
+                .then(() => {
+                    this.isClosing = false
+                    this.closingTimer = 0
+                })
+                .catch(e => e)
         },
         capturePostHog (option) {
             product.capture('clicked-trial-education-link', option)
diff --git a/frontend/src/components/drawers/RightDrawer.vue b/frontend/src/components/drawers/RightDrawer.vue
index dee217c0e1..63ad836a5c 100644
--- a/frontend/src/components/drawers/RightDrawer.vue
+++ b/frontend/src/components/drawers/RightDrawer.vue
@@ -1,5 +1,5 @@
 <template>
-    <section id="right-drawer" v-click-outside="{handler: closeRightDrawer, exclude: ['right-drawer']}" :class="{open: rightDrawer.state}" data-el="right-drawer">
+    <section id="right-drawer" v-click-outside="{handler: closeDrawer, exclude: ['right-drawer']}" :class="{open: rightDrawer.state}" data-el="right-drawer">
         <component :is="rightDrawer.component" v-if="rightDrawer.component" />
     </section>
 </template>
@@ -13,8 +13,14 @@ export default {
         ...mapState('ux', ['rightDrawer'])
     },
     methods: {
-        ...mapActions('ux', ['closeRightDrawer'])
+        ...mapActions('ux', ['closeRightDrawer']),
+        closeDrawer () {
+            if (this.rightDrawer.state) {
+                this.closeRightDrawer()
+            }
+        }
     }
+
 }
 </script>
 
diff --git a/frontend/src/pages/UnverifiedEmail.vue b/frontend/src/pages/UnverifiedEmail.vue
index 29211bef0b..f5c291a7e9 100644
--- a/frontend/src/pages/UnverifiedEmail.vue
+++ b/frontend/src/pages/UnverifiedEmail.vue
@@ -55,7 +55,7 @@ export default {
             try {
                 await userApi.verifyEmailToken(this.token)
                 clearTimeout(this.resendTimeout)
-                this.$store.dispatch('ux/activateTour', 'welcome')
+                this.$store.dispatch('ux/tours/presentTour')
                 this.$store.dispatch('ux/setNewlyCreatedUser', true)
                 this.$router.go()
             } catch (err) {
diff --git a/frontend/src/pages/team/Applications/index.vue b/frontend/src/pages/team/Applications/index.vue
index 4114cc5885..0f044a8c5e 100644
--- a/frontend/src/pages/team/Applications/index.vue
+++ b/frontend/src/pages/team/Applications/index.vue
@@ -108,11 +108,6 @@ import teamApi from '../../../api/team.js'
 import EmptyState from '../../../components/EmptyState.vue'
 import permissionsMixin from '../../../mixins/Permissions.js'
 import Alerts from '../../../services/alerts.js'
-import Tours from '../../../tours/Tours.js'
-
-import TourFirstDevice from '../../../tours/tour-first-device.js'
-import TourWelcomeFree from '../../../tours/tour-welcome-free.json'
-import TourWelcome from '../../../tours/tour-welcome.json'
 
 import ApplicationListItem from './components/Application.vue'
 
@@ -137,8 +132,8 @@ export default {
         }
     },
     computed: {
-        ...mapState('ux', ['tours', 'completeTours']),
-        ...mapGetters('account', ['featuresCheck', 'team']),
+        ...mapState('ux/tours', ['tours', 'completed', 'shouldPresentTour']),
+        ...mapGetters('account', ['featuresCheck', 'team', 'isFreeTeamType']),
         applicationsList () {
             return Array.from(this.applications.values()).map(app => {
                 return {
@@ -218,22 +213,16 @@ export default {
                 count += app.instances.length
                 return count
             }, 0)
-        },
-        isFreemiumTeamType () {
-            return !this.featuresCheck?.isHostedInstancesEnabledForTeam
         }
     },
     watch: {
         team: 'fetchData',
-        tours: {
-            handler (tours) {
-                // handles the user manually re-requesting the tour
-                if (tours.welcome || tours['first-device']) {
+        shouldPresentTour: {
+            handler (should) {
+                if (should) {
                     this.dispatchTour()
                 }
-            },
-            deep: true,
-            immediate: true
+            }
         }
     },
     async mounted () {
@@ -248,7 +237,7 @@ export default {
             })
         }
 
-        if (this.tours.welcome || this.tours['first-device']) {
+        if (this.shouldPresentTour) {
             // given we've loaded resources, check for tour status
             this.dispatchTour()
         }
@@ -342,47 +331,37 @@ export default {
                 this.filterTerm = this.$route.query.searchQuery
             }
         },
-        hasTourBeenCompleted (tour) {
-            return Object.prototype.hasOwnProperty.call(this.completeTours, tour)
-        },
+
         dispatchTour () {
-            if (this.tour) {
-                // don't run two tours at once
-                return
-            }
             switch (true) {
-            case this.isFreemiumTeamType && !this.hasTourBeenCompleted('first-device') && !!this.applicationsList[0]:
+            case this.isFreeTeamType && !!this.applicationsList[0]:
                 // freemium users must first undergo the first-device tour on the ApplicationDevices page
-                return this.$store.dispatch('ux/activateTour', 'first-device')
-                    .then(() => this.$router.push({
-                        name: 'ApplicationDevices',
-                        params: { team_slug: this.team.slug, id: this.applicationsList[0].id }
-                    }))
-                    .then(() => {
-                        this.tour = Tours.create(
-                            'first-device',
-                            TourFirstDevice,
-                            this.$store
-                        )
-                        this.tour.start()
-                    })
+                return this.$router.push({
+                    name: 'ApplicationDevices',
+                    params: { team_slug: this.team.slug, id: this.applicationsList[0].id }
+                })
+                    .then(() => this.$store.dispatch('ux/tours/setFirstDeviceTour'))
                     .catch(e => e)
 
-            case !this.isFreemiumTeamType && this.instanceCount > 0:
-                this.tour = Tours.create('welcome', TourWelcome, this.$store, () => {
-                    this.$store.dispatch('ux/activateTour', 'education')
-                })
+            case !this.isFreeTeamType && this.instanceCount > 0:
                 // Running with an Instance pre-configured (Trial team types)
-                return this.tour.start()
+                return this.$store.dispatch(
+                    'ux/tours/setTrialWelcomeTour',
+                    () => this.$store.dispatch('ux/tours/openModal', 'education')
+                )
+                    .catch(e => e)
 
-            case !this.isFreemiumTeamType:
+            case !this.isFreeTeamType:
                 // any regular team type
-                this.tour = Tours.create('welcome', TourWelcomeFree, this.$store, () => {
-                    if (this.deviceCount === 0) {
-                        this.$store.dispatch('ux/activateTour', 'first-device')
-                    }
-                })
-                return this.tour.start()
+                return this.$store.dispatch(
+                    'ux/tours/setWelcomeTour',
+                    () => {
+                        if (this.deviceCount === 0) {
+                            // start first device tour when finished
+                            this.$store.dispatch('ux/tours/setFirstDeviceTour')
+                        }
+                    })
+                    .catch(e => e)
 
             default:
                 // no tours
diff --git a/frontend/src/store/modules/account/index.js b/frontend/src/store/modules/account/index.js
index 84d43e4dd9..d7cb0d5ed6 100644
--- a/frontend/src/store/modules/account/index.js
+++ b/frontend/src/store/modules/account/index.js
@@ -83,6 +83,9 @@ const getters = {
     team (state) {
         return state.team
     },
+    isFreeTeamType (state) {
+        return !!(state.team?.type?.properties?.billing?.disabled)
+    },
     teamMembership (state) {
         return state.teamMembership
     },
@@ -149,6 +152,7 @@ const getters = {
     featuresCheck: (state) => {
         const preCheck = {
             // Instances
+            // deprecated, use the isFreeTeamType getter
             isHostedInstancesEnabledForTeam: ((state) => {
                 if (!state.team) {
                     return false
diff --git a/frontend/src/store/modules/ux/index.js b/frontend/src/store/modules/ux/index.js
index 5a28959810..05ede1bb51 100644
--- a/frontend/src/store/modules/ux/index.js
+++ b/frontend/src/store/modules/ux/index.js
@@ -11,6 +11,8 @@ import ProjectsIcon from '../../../components/icons/Projects.js'
 import usePermissions from '../../../composables/Permissions.js'
 import { Roles } from '../../../utils/roles.js'
 
+import tours from './tours/index.js'
+
 const initialState = () => ({
     leftDrawer: {
         state: false,
@@ -20,12 +22,6 @@ const initialState = () => ({
         state: false,
         component: null
     },
-    tours: {
-        welcome: false,
-        education: false,
-        'first-device': false
-    },
-    completeTours: {},
     mainNav: {
         context: 'team',
         backToButton: null
@@ -38,10 +34,6 @@ const initialState = () => ({
 
 const meta = {
     persistence: {
-        tours: {
-            storage: 'localStorage'
-            // clearOnLogout: true (cleared by default)
-        },
         isNewlyCreatedUser: {
             storage: 'localStorage'
         },
@@ -57,9 +49,6 @@ const getters = {
     hiddenLeftDrawer: (state, getters) => {
         return state.leftDrawer.component?.name === 'MainNav' && getters.mainNavContext.length === 0
     },
-    shouldShowEducationModal: (state) => {
-        return state.tours.education
-    },
     mainNavContexts: function (state, getters, rootState, rootGetters) {
         const { hasALowerOrEqualTeamRoleThan, hasAMinimumTeamRoleOf, hasPermission } = usePermissions()
         const team = rootState.account.team
@@ -460,23 +449,9 @@ const mutations = {
     setMainNavBackButton (state, button) {
         state.mainNav.backToButton = button
     },
-    activateTour (state, tour) {
-        state.tours[tour] = true
-    },
     setNewlyCreatedUser (state, payload) {
         state.isNewlyCreatedUser = payload
     },
-    deactivateTour (state, tour) {
-        state.tours[tour] = false
-        state.completeTours[tour] = true
-    },
-    resetTours (state) {
-        Object.keys(state.tours)
-            .forEach(key => {
-                state.tours[key] = false
-            })
-        state.completeTours = {}
-    },
     setUserAction (state, { action, payload }) {
         if (Object.prototype.hasOwnProperty.call(state.userActions, action)) {
             state.userActions[action] = payload
@@ -512,15 +487,6 @@ const actions = {
     setNewlyCreatedUser ({ commit }) {
         commit('setNewlyCreatedUser', true)
     },
-    activateTour ({ commit }, tour) {
-        commit('activateTour', tour)
-    },
-    deactivateTour ({ commit, state }, tour) {
-        commit('deactivateTour', tour)
-    },
-    resetTours ({ commit }) {
-        commit('resetTours')
-    },
     validateUserAction ({ commit }, action) {
         commit('setUserAction', { action, payload: true })
     },
@@ -535,6 +501,7 @@ const actions = {
 
 export default {
     namespaced: true,
+    modules: { tours },
     state,
     initialState: initialState(),
     getters,
diff --git a/frontend/src/store/modules/ux/tours/index.js b/frontend/src/store/modules/ux/tours/index.js
new file mode 100644
index 0000000000..ca24b8bd7e
--- /dev/null
+++ b/frontend/src/store/modules/ux/tours/index.js
@@ -0,0 +1,137 @@
+import Tours from '../../../../tours/Tours.js'
+import TourFirstDevice, { id as FirstDeviceTourId } from '../../../../tours/tour-first-device.js'
+import TourWelcomeTrial, { id as TrialWelcomeTourId } from '../../../../tours/tour-welcome-trial.js'
+import TourWelcome, { id as WelcomeTourId } from '../../../../tours/tour-welcome.js'
+
+const initialState = () => ({
+    tours: {
+        [WelcomeTourId]: false,
+        [FirstDeviceTourId]: false,
+        [TrialWelcomeTourId]: false
+    },
+    modals: {
+        education: false
+    },
+    completed: {},
+    activeTour: null,
+    shouldPresentTour: false
+})
+
+const meta = {
+    persistence: {
+        tours: {
+            storage: 'localStorage'
+            // clearOnLogout: true (cleared by default)
+        },
+        completed: {
+            storage: 'localStorage'
+        },
+        shouldPresentTour: {
+            storage: 'localStorage'
+        }
+    }
+}
+
+const state = initialState
+
+const getters = {
+    shouldShowEducationModal: (state) => {
+        return state.modals.education
+    },
+    hasTourBeenCompleted: (state) => (tour) => {
+        return Object.prototype.hasOwnProperty.call(state.completed, tour)
+    }
+}
+
+const mutations = {
+    activateTour (state, tour) {
+        state.tours[tour] = true
+    },
+    deactivateTour (state, tour) {
+        state.tours[tour] = false
+        state.completed[tour] = true
+    },
+    resetTours (state) {
+        Object.keys(state.tours)
+            .forEach(key => {
+                state.tours[key] = false
+            })
+        state.completed = {}
+    },
+    setActiveTour (state, tour) {
+        if (!state.activeTour || !state.activeTour.isActive()) {
+            state.activeTour = tour
+        }
+    },
+    clearActiveTour (state) {
+        state.activeTour = null
+    },
+    presentTour (state) {
+        state.shouldPresentTour = true
+    },
+    withdrawTour (state) {
+        state.shouldPresentTour = false
+    },
+    openModal (state, modal) {
+        state.modals[modal] = true
+    },
+    closeModal (state, modal) {
+        state.modals[modal] = false
+    }
+}
+
+const actions = {
+    activateTour ({ commit }, tour) {
+        commit('activateTour', tour)
+    },
+    deactivateTour ({ commit, state }, tour) {
+        commit('deactivateTour', tour)
+    },
+    resetTours ({ commit }) {
+        commit('resetTours')
+    },
+    setFirstDeviceTour ({ commit, dispatch }, callback = () => { }) {
+        commit('setActiveTour', Tours.create(FirstDeviceTourId, TourFirstDevice, callback))
+        dispatch('startTour')
+    },
+    setTrialWelcomeTour ({ commit, dispatch }, callback = () => { }) {
+        commit('setActiveTour', Tours.create(TrialWelcomeTourId, TourWelcomeTrial, callback))
+        dispatch('startTour')
+    },
+    setWelcomeTour ({ commit, dispatch }, callback = () => { }) {
+        commit('setActiveTour', Tours.create(WelcomeTourId, TourWelcome, callback))
+        dispatch('startTour')
+    },
+    startTour ({ state }) {
+        setTimeout(() => {
+            if (state.activeTour && !state.activeTour.isActive()) {
+                state.activeTour.start()
+            }
+        }, 1000)
+    },
+    clearActiveTour ({ commit }) {
+        commit('clearActiveTour')
+    },
+    presentTour ({ commit }) {
+        commit('presentTour')
+    },
+    withdrawTour ({ commit }) {
+        commit('withdrawTour')
+    },
+    openModal ({ commit }, modal) {
+        commit('openModal', modal)
+    },
+    closeModal ({ commit }, modal) {
+        commit('closeModal', modal)
+    }
+}
+
+export default {
+    namespaced: true,
+    state,
+    initialState: initialState(),
+    getters,
+    mutations,
+    actions,
+    meta
+}
diff --git a/frontend/src/store/plugins/storage.plugin.js b/frontend/src/store/plugins/storage.plugin.js
index ed59f77d00..ba43084d6d 100644
--- a/frontend/src/store/plugins/storage.plugin.js
+++ b/frontend/src/store/plugins/storage.plugin.js
@@ -3,15 +3,19 @@ import SessionStorageService from '../../services/storage/session-storage.servic
 
 const STORE_KEY = 'store'
 
+/**
+ * Filters state by storage type (localStorage or sessionStorage)
+ * Iterates through the state object and retains only the keys that match the specified storage type.
+ */
 const filterByStorageType = (obj, storageType) => {
     return Object.keys(obj).reduce((acc, key) => {
         const item = obj[key]
 
-        // Check if the item has a rules object with storage matching the specified storageType
+        // Check if the item has a persistence rule matching the specified storage type
         if (item && item.rules && item.rules.storage === storageType) {
             acc[key] = item.payload
         } else if (typeof item === 'object' && item !== null && !Array.isArray(item)) {
-            // If the item is an object and doesn't directly match the rules, recurse into it
+            // Recursively filter nested objects
             const filteredSubItem = filterByStorageType(item, storageType)
             if (Object.keys(filteredSubItem).length > 0) {
                 acc[key] = filteredSubItem
@@ -22,34 +26,53 @@ const filterByStorageType = (obj, storageType) => {
     }, {})
 }
 
+/**
+ * Recursively traverses Vuex modules to extract persistence rules for each module.
+ * This allows each module (including nested ones) to have independent persistence settings.
+ */
+const traverseModules = (modules, path = '') => {
+    return Object.keys(modules).reduce((acc, key) => {
+        const fullPath = path ? `${path}/${key}` : key
+        acc[fullPath] = modules[key]._rawModule.meta?.persistence ?? {}
+
+        // Recursively traverse child modules
+        if (modules[key]._children) {
+            Object.assign(acc, traverseModules(modules[key]._children, fullPath))
+        }
+        return acc
+    }, {})
+}
+
+/**
+ * Restores Vuex state from storage while merging nested states.
+ * Ensures that nested modules stored in different storage types are not overridden by their parent modules.
+ */
 function updateVuexFromStorageDriver (store, storageDriver) {
     if (storageDriver.getItem(STORE_KEY)) {
-        // Replace the state object with the stored item
         const currentState = store.state
         const storedState = JSON.parse(storageDriver.getItem(STORE_KEY))
-        Object.keys(storedState)
-            .forEach(storeKey => {
-                Object.keys(storedState[storeKey])
-                    .filter(k => k) // filter out 'global' module
-                    .forEach(key => {
-                        currentState[storeKey][key] = storedState[storeKey][key]
-                    })
-            })
+
+        Object.keys(storedState).forEach(storeKey => {
+            const keys = storeKey.split('/')
+            let targetState = currentState
+
+            // Traverse through nested modules, ensuring that parent modules exist
+            for (let i = 0; i < keys.length - 1; i++) {
+                if (!targetState[keys[i]]) targetState[keys[i]] = {}
+                targetState = targetState[keys[i]]
+            }
+
+            // Merge stored state into the existing module state instead of replacing it
+            Object.assign(targetState[keys[keys.length - 1]], storedState[storeKey])
+        })
     }
 }
 
 export default (store) => {
-    const modules = store._modules.root._children
-    const modulePersistenceRules = {}
-
-    // map each module persistence rules
-    Object.keys(modules)
-        .filter(k => k) // filter out 'global' module
-        .forEach((key) => {
-            modulePersistenceRules[key] = modules[key]._rawModule.meta?.persistence ?? {}
-        })
+    // Extract persistence rules for all Vuex modules, including nested ones
+    const modulePersistenceRules = traverseModules(store._modules.root._children)
 
-    // Register a 'global' module to handle the store initialization
+    // Register an initialization mutation to restore persisted state on startup
     store.registerModule('', {
         mutations: {
             initializeStore: () => {
@@ -59,36 +82,47 @@ export default (store) => {
         }
     })
 
+    // Subscribe to Vuex mutations to persist state whenever a mutation occurs
     store.subscribe((mutation, state) => {
         const result = {}
-        Object.keys(state)
-            .filter(k => k) // filter out 'global' module
-            .forEach((key) => {
-                const storeState = state[key]
-                const statePersistenceRules = modulePersistenceRules[key]
-                const persistentKeys = Object.keys(statePersistenceRules)
-
-                // reducing state to eliminate the keys than do not require persistence
-                result[key] = persistentKeys.reduce((acc, key) => {
-                    if (Object.prototype.hasOwnProperty.call(storeState, key)) {
-                        // setting default localStorage as storage type if not set
-                        if (!statePersistenceRules[key].storage) {
-                            statePersistenceRules[key].storage = 'localStorage'
-                        }
-
-                        acc[key] = {
-                            payload: storeState[key],
-                            rules: statePersistenceRules[key]
-                        }
+
+        Object.keys(modulePersistenceRules).forEach(modulePath => {
+            const keys = modulePath.split('/')
+            let targetState = state
+
+            // Traverse through nested state modules
+            for (const key of keys) {
+                if (!targetState[key]) return
+                targetState = targetState[key]
+            }
+
+            const statePersistenceRules = modulePersistenceRules[modulePath]
+            const persistentKeys = Object.keys(statePersistenceRules)
+
+            // Reduce state to include only the keys that should be persisted
+            result[modulePath] = persistentKeys.reduce((acc, key) => {
+                if (Object.prototype.hasOwnProperty.call(targetState, key)) {
+                    // Default to localStorage if no storage type is set
+                    if (!statePersistenceRules[key].storage) {
+                        statePersistenceRules[key].storage = 'localStorage'
                     }
-                    return acc
-                }, {})
-            })
 
+                    acc[key] = {
+                        payload: targetState[key],
+                        rules: statePersistenceRules[key]
+                    }
+                }
+                return acc
+            }, {})
+        })
+
+        // Persist state to localStorage
         LocalStorageService.setItem(
             STORE_KEY,
             JSON.stringify(filterByStorageType(result, 'localStorage'))
         )
+
+        // Persist state to sessionStorage
         SessionStorageService.setItem(
             STORE_KEY,
             JSON.stringify(filterByStorageType(result, 'sessionStorage'))
diff --git a/frontend/src/tours/Tours.js b/frontend/src/tours/Tours.js
index 9bb263f35d..e3881ed41f 100644
--- a/frontend/src/tours/Tours.js
+++ b/frontend/src/tours/Tours.js
@@ -2,22 +2,21 @@
 import { offset } from '@floating-ui/dom'
 import { useShepherd } from 'vue-shepherd'
 
-import { useStore } from 'vuex'
-
 import Product from '../services/product.js'
 
 // eslint-disable-next-line n/no-extraneous-import
 import 'shepherd.js/dist/css/shepherd.css'
 import './tour-theme.scss'
 
-function create (id, tourJson, store, onCloseHook) {
-    if (!store) {
-        store = useStore()
-    }
+import store from '../store/index.js'
+
+function create (id, tourJson, onCloseHook) {
+    store.dispatch('ux/tours/activateTour', id)
     Product.capture('ff-tour-start', {
         tour_id: id
     })
     const tour = useShepherd({
+        id,
         useModalOverlay: true,
         defaultStepOptions: {
             arrow: true,
@@ -34,7 +33,9 @@ function create (id, tourJson, store, onCloseHook) {
 
     function onCancel () {
         const index = tour.steps.indexOf(tour.currentStep)
-        store.dispatch('ux/deactivateTour', id)
+        store.dispatch('ux/tours/deactivateTour', id)
+        store.dispatch('ux/tours/clearActiveTour')
+        store.dispatch('ux/tours/withdrawTour')
         Product.capture('ff-tour-cancel', {
             tour_id: id,
             tour_step: index
@@ -45,7 +46,9 @@ function create (id, tourJson, store, onCloseHook) {
     }
 
     function onComplete () {
-        store.dispatch('ux/deactivateTour', id)
+        store.dispatch('ux/tours/deactivateTour', id)
+        store.dispatch('ux/tours/clearActiveTour')
+        store.dispatch('ux/tours/withdrawTour')
         Product.capture('ff-tour-complete', {
             tour_id: id
         })
diff --git a/frontend/src/tours/tour-first-device.js b/frontend/src/tours/tour-first-device.js
index 24c44f0a54..64fa676eab 100644
--- a/frontend/src/tours/tour-first-device.js
+++ b/frontend/src/tours/tour-first-device.js
@@ -1,3 +1,5 @@
+export const id = 'first-device'
+
 export default [
     // {
     //     title: 'Application: Remote Instances',
@@ -78,4 +80,5 @@ export default [
                 }, 100)
             })
         }
-    }]
+    }
+]
diff --git a/frontend/src/tours/tour-welcome-free.json b/frontend/src/tours/tour-welcome-free.json
deleted file mode 100644
index a02effd492..0000000000
--- a/frontend/src/tours/tour-welcome-free.json
+++ /dev/null
@@ -1,65 +0,0 @@
-[{
-    "title": "Welcome to FlowFuse!",
-    "text": "<p>Welcome to FlowFuse, the complete platform for building, managing and deploying your Node-RED applications.</p><p><b>Let's take a quick tour to get you started.</b></p>"
-}, {
-    "title": "Concept: Applications",
-    "text": "<p><b>This is your first Application in FlowFuse.</b></p><p>Applications help to organize your Node-RED Instances and other resources.</p>",
-    "attachTo": {
-        "element": "li[data-el=\"application-item\"]",
-        "on": "bottom"
-    },
-    "modalOverlayOpeningXOffset": 12
-}, {
-    "title": "Concept: Hosted Instances",
-    "text": "<p><b>Instances</b> refer to instances of Node-RED, managed through FlowFuse.</p><p><b>Hosted Instances</b> run in the same environment as FlowFuse.</p>",
-    "attachTo": {
-        "element": "section[data-el=\"application-instances-none\"]",
-        "on": "bottom"
-    }
-}, {
-    "title": "Concept: Remote Instances",
-    "text": "<p><b>Remote Instances</b> are Node-RED instances that are managed and deployed onto your own hardware.</p><p>This could be Instances running in factories or on a Raspberry Pi on your desk.</p>",
-    "attachTo": {
-        "element": "section[data-el=\"application-devices-none\"], section[data-el=\"application-devices\"]",
-        "on": "bottom"
-    }
-}, {
-    "title": "Navigation: Application Summary",
-    "text": "<p>Here you can see a summary of the resources within your Application.</p><p>We've already had a quick look at the first two, but let's quickly scan over the others.</p>",
-    "attachTo": {
-        "element": "div[data-el=\"application-summary\"]",
-        "on": "left"
-    }
-}, {
-    "title": "Concept: Pipelines",
-    "text": "<p><b>DevOps Pipelines</b> help you push flows and configuration from one running Node-RED Instance to another.</p></p>This could be from a testing Instance to a production Instance, or from a single (test) Remote Instance out to thousands of (production) Remote Instances in your factory.</p>",
-    "attachTo": {
-        "element": "a[data-nav=\"application-pipelines\"] ",
-        "on": "left"
-    }
-}, {
-    "title": "Concept: Snapshots",
-    "text": "<p><b>Snapshots</b> are a point-in-time backup of a Node-RED Instance. They capture the flows, credentials and runtime settings.</p><p>Snapshots are used mostly for <b>Version Control</b> and in <b>Pipelines</b> where you can choose to roll out a <b>Snapshot</b> to <b>Instances</b> or <b>Device Group</b>.</p>",
-    "attachTo": {
-        "element": "a[data-nav=\"application-snapshots\"] ",
-        "on": "left"
-    }
-}, {
-    "title": "Concept: Device Groups",
-    "text": "<p><b>Device Groups</b> are collections of <b>Remote Instances</b> that can be used in <b>Pipelines</b> in order to deploy the same Node-RED flows out to multiple pieces of hardware in one-click.</p>",
-    "attachTo": {
-        "element": "a[data-nav=\"application-device-groups\"] ",
-        "on": "left"
-    }
-}, {
-    "title": "Setup Your First Remote Instance",
-    "text": "To get started, let's select your Application.</p>",
-    "attachTo": {
-        "element": "a[data-action=\"view-application\"]",
-        "on": "bottom"
-    },
-    "advanceOn": {
-        "selector": "a[data-action=\"view-application\"]",
-        "event": "click"
-    }
-}]
diff --git a/frontend/src/tours/tour-welcome-trial.js b/frontend/src/tours/tour-welcome-trial.js
new file mode 100644
index 0000000000..4c75f4bddb
--- /dev/null
+++ b/frontend/src/tours/tour-welcome-trial.js
@@ -0,0 +1,72 @@
+export const id = 'welcome-trial'
+export default [
+    {
+        title: 'Welcome to FlowFuse!',
+        text: '<p>Welcome to FlowFuse, the complete platform for building, managing and deploying your Node-RED applications.</p><p><b>Let\'s take a quick tour to get you started.</b></p>'
+    },
+    {
+        title: 'Concept: Applications',
+        text: '<p><b>This is your first Application in FlowFuse.</b></p><p>Applications help to organize your Node-RED Instances and other resources.</p>',
+        attachTo: {
+            element: 'li[data-el="application-item"]',
+            on: 'bottom'
+        },
+        modalOverlayOpeningXOffset: 12
+    },
+    {
+        title: 'Concept: Hosted Instances',
+        text: '<p><b>Hosted Instances</b> refer to instances of Node-RED running at the <i>same</i> host as FlowFuse.</p>',
+        attachTo: {
+            element: '[data-el="application-instance-item"]:first-of-type',
+            on: 'bottom'
+        }
+    },
+    {
+        title: 'Concept: Remote Instances',
+        text: '<p><b>Remote Instances</b> are Node-RED instances that are managed and deployed <i>remotely</i>.</p><p>Whether hardware in factories or a Raspberry Pi on your desk, FlowFuse helps you manage hundreds of remote deployments.</p>',
+        attachTo: {
+            element: 'section[data-el="application-devices-none"], section[data-el="application-devices"]',
+            on: 'bottom'
+        }
+    },
+    {
+        title: 'Navigation: Application Summary',
+        text: '<p>Here you can see a summary of the resources within your Application.</p><p>We\'ve already had a quick look at the first two, but let\'s quickly scan over the others.</p>',
+        attachTo: {
+            element: 'div[data-el="application-summary"]',
+            on: 'left'
+        }
+    },
+    {
+        title: 'Concept: Pipelines',
+        text: '<p><b>DevOps Pipelines</b> help you push flows and configuration from one running Node-RED to another.</p></p>This could be from a testing instance to a production instance, or from a single test device out to thousands of devices in your factory.</p>',
+        attachTo: {
+            element: 'a[data-nav="application-pipelines"] ',
+            on: 'left'
+        }
+    },
+    {
+        title: 'Concept: Snapshots',
+        text: '<p><b>Snapshots</b> are a point-in-time backup of a Node-RED instance. They capture the flows, credentials and runtime settings.</p><p>Snapshots are used mostly for <b>Version Control</b> and in <b>Pipelines</b> where you can choose to roll out a <b>Snapshot</b> to another <b>Instance</b>, <b>Device</b> or <b>Device Group</b>.</p>',
+        attachTo: {
+            element: 'a[data-nav="application-snapshots"] ',
+            on: 'left'
+        }
+    },
+    {
+        title: 'Concept: Device Groups',
+        text: '<p><b>Device Groups</b> are collections of <b>Remote Instances</b> that can be used in <b>Pipelines</b> in order to deploy the same Node-RED flows out to multiple pieces of hardware in one-click.</p>',
+        attachTo: {
+            element: 'a[data-nav="application-device-groups"] ',
+            on: 'left'
+        }
+    },
+    {
+        title: 'Open Node-RED Editor',
+        text: '<p><b>That\'s our quick introduction to FlowFuse.</b></p><p>When you\'re ready, we recommend that you open a Node-RED editor and start building your first flow.</p>',
+        attachTo: {
+            element: 'div[data-el="application-instance-item"]',
+            on: 'bottom'
+        }
+    }
+]
diff --git a/frontend/src/tours/tour-welcome.js b/frontend/src/tours/tour-welcome.js
new file mode 100644
index 0000000000..d93fbe0cb0
--- /dev/null
+++ b/frontend/src/tours/tour-welcome.js
@@ -0,0 +1,77 @@
+export const id = 'welcome'
+
+export default [
+    {
+        title: 'Welcome to FlowFuse!',
+        text: "<p>Welcome to FlowFuse, the complete platform for building, managing and deploying your Node-RED applications.</p><p><b>Let's take a quick tour to get you started.</b></p>"
+    },
+    {
+        title: 'Concept: Applications',
+        text: '<p><b>This is your first Application in FlowFuse.</b></p><p>Applications help to organize your Node-RED Instances and other resources.</p>',
+        attachTo: {
+            element: 'li[data-el="application-item"]',
+            on: 'bottom'
+        },
+        modalOverlayOpeningXOffset: 12
+    },
+    {
+        title: 'Concept: Hosted Instances',
+        text: '<p><b>Instances</b> refer to instances of Node-RED, managed through FlowFuse.</p><p><b>Hosted Instances</b> run in the same environment as FlowFuse.</p>',
+        attachTo: {
+            element: 'section[data-el="application-instances-none"]',
+            on: 'bottom'
+        }
+    },
+    {
+        title: 'Concept: Remote Instances',
+        text: '<p><b>Remote Instances</b> are Node-RED instances that are managed and deployed onto your own hardware.</p><p>This could be Instances running in factories or on a Raspberry Pi on your desk.</p>',
+        attachTo: {
+            element: 'section[data-el="application-devices-none"], section[data-el="application-devices"]',
+            on: 'bottom'
+        }
+    },
+    {
+        title: 'Navigation: Application Summary',
+        text: "<p>Here you can see a summary of the resources within your Application.</p><p>We've already had a quick look at the first two, but let's quickly scan over the others.</p>",
+        attachTo: {
+            element: 'div[data-el="application-summary"]',
+            on: 'left'
+        }
+    },
+    {
+        title: 'Concept: Pipelines',
+        text: '<p><b>DevOps Pipelines</b> help you push flows and configuration from one running Node-RED Instance to another.</p></p>This could be from a testing Instance to a production Instance, or from a single (test) Remote Instance out to thousands of (production) Remote Instances in your factory.</p>',
+        attachTo: {
+            element: 'a[data-nav="application-pipelines"] ',
+            on: 'left'
+        }
+    },
+    {
+        title: 'Concept: Snapshots',
+        text: '<p><b>Snapshots</b> are a point-in-time backup of a Node-RED Instance. They capture the flows, credentials and runtime settings.</p><p>Snapshots are used mostly for <b>Version Control</b> and in <b>Pipelines</b> where you can choose to roll out a <b>Snapshot</b> to <b>Instances</b> or <b>Device Group</b>.</p>',
+        attachTo: {
+            element: 'a[data-nav="application-snapshots"] ',
+            on: 'left'
+        }
+    },
+    {
+        title: 'Concept: Device Groups',
+        text: '<p><b>Device Groups</b> are collections of <b>Remote Instances</b> that can be used in <b>Pipelines</b> in order to deploy the same Node-RED flows out to multiple pieces of hardware in one-click.</p>',
+        attachTo: {
+            element: 'a[data-nav="application-device-groups"] ',
+            on: 'left'
+        }
+    },
+    {
+        title: 'Setup Your First Remote Instance',
+        text: "To get started, let's select your Application.</p>",
+        attachTo: {
+            element: 'a[data-action="view-application"]',
+            on: 'bottom'
+        },
+        advanceOn: {
+            selector: 'a[data-action="view-application"]',
+            event: 'click'
+        }
+    }
+]
diff --git a/frontend/src/tours/tour-welcome.json b/frontend/src/tours/tour-welcome.json
deleted file mode 100644
index 6a407a4d41..0000000000
--- a/frontend/src/tours/tour-welcome.json
+++ /dev/null
@@ -1,61 +0,0 @@
-[{
-    "title": "Welcome to FlowFuse!",
-    "text": "<p>Welcome to FlowFuse, the complete platform for building, managing and deploying your Node-RED applications.</p><p><b>Let's take a quick tour to get you started.</b></p>"
-}, {
-    "title": "Concept: Applications",
-    "text": "<p><b>This is your first Application in FlowFuse.</b></p><p>Applications help to organize your Node-RED Instances and other resources.</p>",
-    "attachTo": {
-        "element": "li[data-el=\"application-item\"]",
-        "on": "bottom"
-    },
-    "modalOverlayOpeningXOffset": 12
-}, {
-    "title": "Concept: Hosted Instances",
-    "text": "<p><b>Hosted Instances</b> refer to instances of Node-RED running at the <i>same</i> host as FlowFuse.</p>",
-    "attachTo": {
-        "element": "[data-el=\"application-instance-item\"]",
-        "on": "bottom"
-    }
-}, {
-    "title": "Concept: Remote Instances",
-    "text": "<p><b>Remote Instances</b> are Node-RED instances that are managed and deployed <i>remotely</i>.</p><p>Whether hardware in factories or a Raspberry Pi on your desk, FlowFuse helps you manage hundreds of remote deployments.</p>",
-    "attachTo": {
-        "element": "section[data-el=\"application-devices-none\"], section[data-el=\"application-devices\"]",
-        "on": "bottom"
-    }
-}, {
-    "title": "Navigation: Application Summary",
-    "text": "<p>Here you can see a summary of the resources within your Application.</p><p>We've already had a quick look at the first two, but let's quickly scan over the others.</p>",
-    "attachTo": {
-        "element": "div[data-el=\"application-summary\"]",
-        "on": "left"
-    }
-}, {
-    "title": "Concept: Pipelines",
-    "text": "<p><b>DevOps Pipelines</b> help you push flows and configuration from one running Node-RED to another.</p></p>This could be from a testing instance to a production instance, or from a single test device out to thousands of devices in your factory.</p>",
-    "attachTo": {
-        "element": "a[data-nav=\"application-pipelines\"] ",
-        "on": "left"
-    }
-}, {
-    "title": "Concept: Snapshots",
-    "text": "<p><b>Snapshots</b> are a point-in-time backup of a Node-RED instance. They capture the flows, credentials and runtime settings.</p><p>Snapshots are used mostly for <b>Version Control</b> and in <b>Pipelines</b> where you can choose to roll out a <b>Snapshot</b> to another <b>Instance</b>, <b>Device</b> or <b>Device Group</b>.</p>",
-    "attachTo": {
-        "element": "a[data-nav=\"application-snapshots\"] ",
-        "on": "left"
-    }
-}, {
-    "title": "Concept: Device Groups",
-    "text": "<p><b>Device Groups</b> are collections of <b>Remote Instances</b> that can be used in <b>Pipelines</b> in order to deploy the same Node-RED flows out to multiple pieces of hardware in one-click.</p>",
-    "attachTo": {
-        "element": "a[data-nav=\"application-device-groups\"] ",
-        "on": "left"
-    }
-}, {
-    "title": "Open Node-RED Editor",
-    "text": "<p><b>That's our quick introduction to FlowFuse.</b></p><p>When you're ready, we recommend that you open a Node-RED editor and start building your first flow.</p>",
-    "attachTo": {
-        "element": "div[data-el=\"application-instance-item\"]",
-        "on": "bottom"
-    }
-}]


Reviews:
user: login: joepavitt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T15:33:05Z
user: login: cstns
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T11:23:15Z
user: login: joepavitt
body: Tours not showing for me when I sign up as a Free Tier user on localhost
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T13:32:02Z
user: login: joepavitt
body: The delay after selecting "Welcome Tour" is too long and needs reducing. Other than that, this is good to go
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T12:39:49Z
user: login: joepavitt
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T12:42:12Z

Review comments:
path: frontend/src/store/modules/ux/modules/tours.js
user: login: joepavitt
body: Rather than needing two folders called `modules`, why not change the `ux.js` to `index.js` then each named file in the `/modules/ux` folder is a directory of `ux`?

Also aligns with https://github.com/FlowFuse/flowfuse/pull/5251/files#r1987529074
created_at: 2025-03-10T15:33:01Z
author_association: CONTRIBUTOR
path: frontend/src/store/modules/ux/modules/tours.js
user: login: cstns
body: that would work quite nicely too
created_at: 2025-03-12T11:23:15Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-07T14:38:19Z
body: ## [Codecov](https://app.codecov.io/gh/FlowFuse/flowfuse/pull/5269?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FlowFuse) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 78.15%. Comparing base [(`d1aa795`)](https://app.codecov.io/gh/FlowFuse/flowfuse/commit/d1aa795ee4ae3ccb35ade7efa9236919633ac48f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FlowFuse) to head [(`e1bfeb7`)](https://app.codecov.io/gh/FlowFuse/flowfuse/commit/e1bfeb709dc98ac94b19e494c0cda4118f873c12?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FlowFuse).
> Report is 80 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #5269   +/-   ##
=======================================
  Coverage   78.14%   78.15%           
=======================================
  Files         340      340           
  Lines       16192    16195    +3     
  Branches     3756     3757    +1     
=======================================
+ Hits        12654    12657    +3     
  Misses       3538     3538           
```

| [Flag](https://app.codecov.io/gh/FlowFuse/flowfuse/pull/5269/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FlowFuse) | Coverage Δ | |
|---|---|---|
| [backend](https://app.codecov.io/gh/FlowFuse/flowfuse/pull/5269/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FlowFuse) | `78.15% <ø> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FlowFuse#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/FlowFuse/flowfuse/pull/5269?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FlowFuse).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FlowFuse).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: cstns
created_at: 2025-03-14T12:42:43Z
body: I noticed that there's a vuex plugin error thrown when switching back and forth from this branch to others given the the store's storage plugin was changed. This is to be expected for dev environments as there might be diffrences between the new storage format and existing.

On production it should not be exhibited rolling forward unless rolling backwards to a version prior to this pr's merge is preformed.

The fix would be an easy one (clear localStorage and sessionStorage) but it would need to be done client-side.

@joepavitt, should I account for this?

![image](https://github.com/user-attachments/assets/6b3a9099-def5-4af3-8d42-a5cf6c0d6893)

author_association: CONTRIBUTOR
user: login: cstns
created_at: 2025-03-14T13:37:52Z
body: give em a a couple of moments to check
author_association: CONTRIBUTOR
user: login: cstns
created_at: 2025-03-14T13:45:18Z
body: that's strange, it does for me (while also replicating the bug, the tours starting twice). Checked on chrome & ffox with full user signup
author_association: CONTRIBUTOR
user: login: joepavitt
created_at: 2025-03-14T14:54:34Z
body: <img width="1836" alt="Screenshot 2025-03-14 at 14 54 21" src="https://github.com/user-attachments/assets/0e1819e6-6741-4eed-8c27-1ff997d133ea" />

cancelling mid-tour leaves me like this, it's also not clearing the Product tour entries in the vuex store, and so replays on every page refresh
author_association: CONTRIBUTOR
user: login: joepavitt
created_at: 2025-03-14T16:37:11Z
body: Okay, the above issue aside, if I sign up for a different account type, i.e. one with a Trial, I get shown the wrong tour too.
author_association: CONTRIBUTOR
user: login: joepavitt
created_at: 2025-03-14T16:40:03Z
body: Have been informed that the above problems are fixed in https://github.com/FlowFuse/flowfuse/pull/5307 

As such, I'll review that, merge that into here if it's okay, and merge all together
author_association: CONTRIBUTOR
user: login: cstns
created_at: 2025-03-14T16:42:08Z
body: yes, that would be the best course of action in this situation. The only thing remaining is [this](https://github.com/FlowFuse/flowfuse/pull/5269#issuecomment-2724571729) comment with the rolling forward

author_association: CONTRIBUTOR
user: login: joepavitt
created_at: 2025-03-19T10:07:34Z
body: Reviewing this again, the "Welcome Tour" button isn't working
author_association: CONTRIBUTOR
user: login: cstns
created_at: 2025-03-24T13:03:54Z
body: > Reviewing this again, the "Welcome Tour" button isn't working

Can you give me a hint of the context it's not working in? I checked locally and on pre-staging and can't replicate it
author_association: CONTRIBUTOR