Skip to content

Commit 96ffb5c

Browse files
AchoArnoldCopilot
andauthored
feat(web): redirect logged-in users from root to /threads (opt-in) (#950)
* docs: add design spec for root->threads redirect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce * docs: add implementation plan for root->threads redirect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce * feat(web): add redirectPreference store for per-browser threads redirect * feat(web): optimistically redirect root to /threads when opted in Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(web): add redirect opt-in popover component * feat(web): mount redirect opt-in popover under Dashboard button * feat(web): clear threads-redirect preference on logout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(web): use replace navigation in redirect enable() to avoid stuck Back button Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce * ci(web): pin pnpm to 11.11.0 to avoid installer bug pnpm 11.12.0 crashes pnpm/action-setup (pnpm/action-setup#276). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce * fix(web): import ref from vue, drop gitignored docs Resolves Codacy type findings; removes gitignored docs/ files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce * fix(web): persist redirect before navigation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 878f9e23-78ea-4f5d-aed2-c1381514107f * fix(web): gate landing nav auth buttons on authStateChanged On CTRL+R reload of /, Firebase restores the session asynchronously. The website layout rendered Login/Get Started while authUser was still null, hiding the Dashboard button until the state resolved. Wait for authStateChanged before showing any auth-dependent button, matching the default (dashboard) layout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 878f9e23-78ea-4f5d-aed2-c1381514107f * refactor(web): render redirect prompt as v-menu not v-card Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 878f9e23-78ea-4f5d-aed2-c1381514107f --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 129dced commit 96ffb5c

8 files changed

Lines changed: 144 additions & 13 deletions

File tree

.github/workflows/web.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
- uses: pnpm/action-setup@v6
3535
name: Install pnpm
3636
with:
37-
version: 11
37+
version: 11.11.0
3838

3939
- name: Install dependencies 📦
4040
run: pnpm install
@@ -66,7 +66,7 @@ jobs:
6666
- uses: pnpm/action-setup@v6
6767
name: Install pnpm
6868
with:
69-
version: 11
69+
version: 11.11.0
7070

7171
- name: Install dependencies 📦
7272
run: pnpm install

web/app/components/MessageThreadHeader.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const phonesStore = usePhonesStore()
2727
const threadsStore = useThreadsStore()
2828
const appStore = useAppStore()
2929
const notificationsStore = useNotificationsStore()
30+
const redirectPreferenceStore = useRedirectPreferenceStore()
3031
3132
const selectedMenuItem = ref(-1)
3233
@@ -71,6 +72,7 @@ async function logout() {
7172
authStore.resetState()
7273
phonesStore.resetState()
7374
threadsStore.resetState()
75+
redirectPreferenceStore.resetState()
7476
notificationsStore.addNotification({
7577
type: 'info',
7678
message: 'You have successfully logged out',
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import { mdiClose, mdiArrowRight } from '@mdi/js'
4+
5+
const route = useRoute()
6+
const authStore = useAuthStore()
7+
const redirectStore = useRedirectPreferenceStore()
8+
9+
const showPopover = computed(
10+
() =>
11+
route.name === 'index' &&
12+
authStore.authUser !== null &&
13+
!redirectStore.enabled &&
14+
!redirectStore.dismissedThisSession,
15+
)
16+
17+
const menuOpen = computed({
18+
get: () => showPopover.value,
19+
set: (value: boolean) => {
20+
if (!value) redirectStore.dismiss()
21+
},
22+
})
23+
</script>
24+
25+
<template>
26+
<v-menu
27+
v-model="menuOpen"
28+
activator="parent"
29+
location="bottom end"
30+
offset="8"
31+
:open-on-click="false"
32+
:close-on-content-click="false"
33+
>
34+
<v-list width="280" class="py-0" rounded="lg" elevation="8">
35+
<v-list-item>
36+
<v-list-item-title class="text-body-1">
37+
Skip this page next time?
38+
</v-list-item-title>
39+
<template #append>
40+
<v-btn
41+
:icon="mdiClose"
42+
variant="text"
43+
size="small"
44+
color="warning"
45+
density="comfortable"
46+
aria-label="Dismiss"
47+
@click="redirectStore.dismiss()"
48+
/>
49+
</template>
50+
</v-list-item>
51+
<v-list-item class="text-primary" @click="redirectStore.enable()">
52+
<v-list-item-title>Always open dashboard</v-list-item-title>
53+
<template #append>
54+
<v-icon :icon="mdiArrowRight" size="small" />
55+
</template>
56+
</v-list-item>
57+
</v-list>
58+
</v-menu>
59+
</template>

web/app/layouts/website.vue

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,11 @@ function goToPricing() {
8080
Blog
8181
</v-btn>
8282
<v-btn
83-
v-show="lgAndUp && authStore.authUser === null"
83+
v-show="
84+
lgAndUp &&
85+
authStore.authStateChanged &&
86+
authStore.authUser === null
87+
"
8488
size="large"
8589
variant="text"
8690
color="primary"
@@ -90,7 +94,7 @@ function goToPricing() {
9094
Login
9195
</v-btn>
9296
<v-btn
93-
v-show="authStore.authUser === null"
97+
v-show="authStore.authStateChanged && authStore.authUser === null"
9498
color="primary"
9599
variant="flat"
96100
:class="{ 'mt-5': mdAndUp, 'mt-1': !mdAndUp }"
@@ -100,16 +104,21 @@ function goToPricing() {
100104
Get Started
101105
<span v-show="lgAndUp">&nbsp;For Free</span>
102106
</v-btn>
103-
<v-btn
104-
v-show="authStore.authUser !== null"
105-
color="primary"
106-
variant="flat"
107-
:class="{ 'mt-5': mdAndUp, 'mt-1': !mdAndUp }"
108-
:size="lgAndUp ? 'large' : 'default'"
109-
:to="{ name: 'threads' }"
107+
<div
108+
v-show="authStore.authStateChanged && authStore.authUser !== null"
109+
class="position-relative d-inline-block"
110110
>
111-
Dashboard
112-
</v-btn>
111+
<v-btn
112+
color="primary"
113+
variant="flat"
114+
:class="{ 'mt-5': mdAndUp, 'mt-1': !mdAndUp }"
115+
:size="lgAndUp ? 'large' : 'default'"
116+
:to="{ name: 'threads' }"
117+
>
118+
Dashboard
119+
</v-btn>
120+
<RedirectPromptPopover />
121+
</div>
113122
</v-col>
114123
</v-row>
115124
</v-container>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { STORAGE_KEY } from '~/stores/redirectPreference'
2+
3+
export default defineNuxtRouteMiddleware(() => {
4+
try {
5+
if (localStorage.getItem(STORAGE_KEY) === 'true') {
6+
return navigateTo('/threads', { replace: true })
7+
}
8+
} catch (error) {
9+
console.error(error)
10+
}
11+
})

web/app/pages/index.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
2929
definePageMeta({
3030
layout: 'website',
31+
middleware: ['redirect-to-threads'],
3132
})
3233
3334
useSeoMeta({

web/app/pages/settings/index.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const authStore = useAuthStore()
4747
const phonesStore = usePhonesStore()
4848
const billingStore = useBillingStore()
4949
const notificationsStore = useNotificationsStore()
50+
const redirectPreferenceStore = useRedirectPreferenceStore()
5051
5152
const firebaseUser = ref<FirebaseUser | null>(null)
5253
const gravatarUrl = ref<string | null>(null)
@@ -773,6 +774,7 @@ async function deleteUserAccount() {
773774
await signOut(auth)
774775
authStore.resetState()
775776
phonesStore.resetState()
777+
redirectPreferenceStore.resetState()
776778
notificationsStore.addNotification({
777779
type: 'info',
778780
message: 'You have successfully logged out',
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { defineStore } from 'pinia'
2+
import { ref } from 'vue'
3+
4+
export const STORAGE_KEY = 'httpsms_redirect_to_threads'
5+
6+
function readFlag(): boolean {
7+
try {
8+
return localStorage.getItem(STORAGE_KEY) === 'true'
9+
} catch (error) {
10+
console.error(error)
11+
return false
12+
}
13+
}
14+
15+
export const useRedirectPreferenceStore = defineStore(
16+
'redirectPreference',
17+
() => {
18+
const enabled = ref(readFlag())
19+
const dismissedThisSession = ref(false)
20+
21+
function enable() {
22+
try {
23+
localStorage.setItem(STORAGE_KEY, 'true')
24+
enabled.value = true
25+
navigateTo('/threads', { replace: true })
26+
} catch (error) {
27+
console.error(error)
28+
}
29+
}
30+
31+
function dismiss() {
32+
dismissedThisSession.value = true
33+
}
34+
35+
function resetState() {
36+
enabled.value = false
37+
dismissedThisSession.value = false
38+
try {
39+
localStorage.removeItem(STORAGE_KEY)
40+
} catch (error) {
41+
console.error(error)
42+
}
43+
}
44+
45+
return { enabled, dismissedThisSession, enable, dismiss, resetState }
46+
},
47+
)

0 commit comments

Comments
 (0)