Ionic 7 + Vue 3 - post logout navigation - view not changing

I’m working on a mobile app using Ionic with Vue 3 for the frontend, and I’ve set up routing using Vue Router to manage navigation between different pages in the app. My backend API is built with Laravel, handling sanctum authentication based on google token from the frontend, and other server-side logic.

App’s Routing Setup

In my router/index.ts, I’ve defined several routes:

  • Login Page (LoginPage.vue): The entry point of the app, where users can log in.
  • Choose Package Page (ChoosePackagePage.vue): A page accessible after logging in, allowing users to select a package.
  • Game Page and End Game Page (GamePage.vue and EndGamePage.vue): Part of the app’s core functionality, where users can play a game and see their results at the end.
  • Social Login (GamePage.vue reused): An alternative login method.
import {createRouter, createWebHistory} from '@ionic/vue-router';
import {RouteRecordRaw} from 'vue-router';
import {SecureStoragePlugin} from 'capacitor-secure-storage-plugin';
import useAuthStore from "@/store/auth/auth";

import ChoosePackagePage from "@/pages/main/ChoosePackagePage.vue";
import GamePage from "@/pages/game/GamePage.vue";
import LoginPage from "@/pages/auth/LoginPage.vue";
import EndGamePage from "@/pages/game/EndGamePage.vue";

const routes: Array<RouteRecordRaw> = [
    {
        path: '/',
        name: 'login',
        component: LoginPage,
        meta: {
            requiresAuth: false,
        },
    },
    {
        path: '/home',
        name: 'home',
        component: ChoosePackagePage,
        meta: {
            requiresAuth: true,
        },
    },
    {
        path: '/game',
        name: 'game',
        component: GamePage,
        meta: {
            requiresAuth: true,
        },
    },
    {
        path: '/end-game',
        name: 'end-game',
        component: EndGamePage,
        meta: {
            requiresAuth: true,
        },
    },
    // {
    //     path: '/social-login',
    //     name: 'social-login',
    //     component: GamePage,
    //     meta: {
    //         requiresAuth: false,
    //     },
    // }
];

const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes,
})

router.beforeEach(async (to, from) => {
    const authStore = useAuthStore();

    if (to.name === 'login' && authStore.isAuthenticated) {
        return {name: 'home'};
    }

    if (to.meta.requiresAuth && !authStore.isAuthenticated) {
        try {
            console.log('trying to get to protected route, and not authenticated, checking for token in secure storage.')
            const result = await SecureStoragePlugin.get({key: 'authToken'});

            if (result.value) {
                console.log('get the token from secure storage, setting it in the store and redirecting to home page.')
                authStore.token = result.value; // Update the token in the store if found
                return {path: '/home'};
            }

            console.log('no token found in secure storage, redirecting to login page.')
            return {name: 'login'};
        } catch (error) {
            if (to.meta.requiresAuth) {
                console.log('error getting token from secure storage, redirecting to login page.')
                return {name: 'login'};
            }
        }
    }

    console.log('nothing returned so just continue to the route.', to.name, from.name)
});


export default router;

The router is configured with a beforeEach navigation guard to check if the user is authenticated (using a Pinia store authStore.isAuthenticated) and redirects to the login page if not authenticated, except for routes marked with requiresAuth: false.

Logout Process

The logout functionality is triggered by a button in my AppLayout.vue(for now until I get it all working then I will clean it up). When clicked, it:

  1. Calls the logout action from authStore, which clears the user’s authentication token from both the app’s state and secure storage.
  2. Uses ionRouter to navigate back to the login page.
const logout = async () => {
  await menuController.close();
  await authStore.logout();
  await ionRouter.push('/');
}

This process works as intended, and upon logout, the app should redirect users back to the LoginPage.vue.

Issue Encountered

After successfully logging in, I can navigate to the Choose Package Page without any issues, indicating that the login process and subsequent navigation work correctly. However, after logging out, although the user data is gone as it should be, the actual view doesn’t update to show the LoginPage.vue until I programatically refresh the view - but I can tell I am logged out because I am unable to click through to other parts of the app after I click the logout button.

What Works

  • Logging in and navigating to other pages based on authentication state works flawlessly.
  • The logout process successfully clears the authentication state and attempts to navigate to the login page because I can see the console log console.log('nothing returned so just continue to the route.', to.name, from.name) and to and from have correct values.

The Problem

  • Despite user data being gone after the logout, the view doesn’t update until a programatic page refresh is performed.

I’ve ensured that each page component, especially LoginPage.vue and ChoosePackagePage.vue, correctly uses <IonPage> to wrap the content, adhering to Ionic’s requirements for navigation and page management. They both have access to <IonPage> via my layouts, SimpleLayout and AppLayout.

I’m seeking advice on ensuring that after logging out, the app correctly updates the view to show the LoginPage.vue immediately, without requiring a manual refresh. I must be doing something silly I just cannot see it.

These are my layouts:

simple:

<script setup lang="ts">
import {IonContent, IonPage} from '@ionic/vue';
</script>

<template>
  <ion-page>
    <ion-content>
      <div class="flex justify-center items-center h-screen">
        <slot/>
      </div>
    </ion-content>
  </ion-page>
</template>

AppLayout

<script setup lang="ts">
import {
  IonContent,
  IonPage,
  IonHeader,
  IonMenu,
  IonIcon,
  IonTitle,
  IonToolbar,
  IonMenuToggle,
  IonList,
  IonItem,
  useIonRouter
} from '@ionic/vue'
import {menu, logOutOutline} from "ionicons/icons";
import {computed} from "vue";
import useAuthStore from "@/store/auth/auth";
import {menuController} from '@ionic/core/components';

const authStore = useAuthStore();
const ionRouter = useIonRouter();

const baseUrl = process.env.NODE_ENV === 'production' ? 'assets/img/' : '/assets/img/';
const backgroundImageUrl = computed(() => `${baseUrl}splash-pattern.svg`);

const logout = async () => {
  await menuController.close();
  await authStore.logout();
  await ionRouter.push('/');
}
</script>

<template>
  <ion-menu side="end" content-id="main-content">
    <ion-header>
      <ion-toolbar>
        <ion-title>Menu</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content class="ion-padding">
      <ion-list class="item-background-color">
        <ion-item>
          <ion-icon :icon="logOutOutline" class="w-10 h-10 mr-2 text-dark-green"></ion-icon>
          <ion-label class="text-xl font-bold text-dark-green" @click="logout">Logout</ion-label>
        </ion-item>
      </ion-list>
    </ion-content>
  </ion-menu>

  <ion-page id="main-content" class="relative">
    <ion-content>
      <ion-menu-toggle class="absolute top-3 right-3 z-index-[9999999]">
        <ion-icon :icon="menu" class="w-10 h-10"></ion-icon>
      </ion-menu-toggle>
      <div
          class="bg-medium-salmon w-full flex flex-col items-center "
          :style="{ backgroundImage: `url(${backgroundImageUrl})`, backgroundSize: '267%' }"
      >

        <slot/>
      </div>
    </ion-content>
  </ion-page>
</template>

<style scoped>
ion-toolbar {
  --background: #e75571;
}

ion-menu ion-content {
  --background: #f6a8b7;
}

.item-background-color {
  --ion-item-background: none;
  --ion-border-color: #f6a8b7;
}
</style>
1 Like

If your routing works correctly, try to force render the LoginPage.vue.

<template>
  <ion-page :key="key">
    ...
  </ion-page>
</template>

<script>
export default {
  data() {
    return {
      key: 0,
    };
  },
  methods: {
    forceRender() {
      this.key+= 1;  
    },
  },
  mounted() {
    Events.addListener('user:logout', this.forceRender);
  },
  beforeDestroy() {
    Events.removeListener('user:logout', this.forceRender);
  }
};
</script>

You need to emit an event from the logout function.

import { Events } from '@ionic/core';

const logout = async () => {
  await menuController.close();
  await authStore.logout();
  await ionRouter.push('/');
  Events.publish('user:logout');
}

Hey! Great to see another Laravel/Vue/Pinia user!!! Also, great details!

I am not seeing anything jumping out that is wrong. I have a very similar set up and in my logout method, I do the following (note importing Vue’s router and not Ionic’s, I’ve seen weird results sometimes when using Ionic’s router wrapper):

import router from '@/router'

static async logout(): Promise<void> {
  // Wipe user data, etc.

  router.replace({ name: 'account.login' })
}

If you cannot get it working with trying Vue’s router or quantumQuester9’s suggestion, can you create a minimal reproduction of it for us to test?

1 Like

Hey Guys,

Thank you for the quick response! :slight_smile:

I had some time to fiddle with that last night finally and I know what was the problem. It turns out that the navigation must entirely be contained within the IonPage element.

<ion-page class="relative">
    <ion-menu side="end" content-id="main-content">
      <ion-header>
        <ion-toolbar>
          <ion-title>Menu</ion-title>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        <ion-list class="item-background-color">
          <ion-item>
            <ion-icon :icon="logOutOutline" class="w-10 h-10 mr-2 text-dark-green"></ion-icon>
            <ion-label class="text-xl font-bold text-dark-green" @click="logout">Logout</ion-label>
          </ion-item>
        </ion-list>
      </ion-content>
    </ion-menu>
    <ion-content  id="main-content">
      <ion-menu-toggle class="absolute top-3 right-3 z-index-[9999999]">
        <ion-icon :icon="menu" class="w-10 h-10"></ion-icon>
      </ion-menu-toggle>
      <div
          class="bg-medium-salmon w-full flex flex-col items-center "
          :style="{ backgroundImage: `url(${backgroundImageUrl})`, backgroundSize: '267%' }"
      >
        <slot/>
      </div>
    </ion-content>
  </ion-page>

This fixed my issue. Now onto another one :smiley: Thanks!

2 Likes