Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remember whether the welcome screen was seen #2375

Merged
merged 6 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/controller/actions/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -668,5 +668,6 @@ export default (sbp('sbp/selectors/register', {
return sbp('okTurtles.eventQueue/queueEvent', 'ACTIONS-LOGIN', ['gi.actions/identity/_private/logout', ...params])
},
...encryptedAction('gi.actions/identity/saveFileDeleteToken', L('Failed to save delete tokens for the attachments.')),
...encryptedAction('gi.actions/identity/removeFileDeleteToken', L('Failed to remove delete tokens for the attachments.'))
...encryptedAction('gi.actions/identity/removeFileDeleteToken', L('Failed to remove delete tokens for the attachments.')),
...encryptedAction('gi.actions/identity/setGroupAttributes', L('Failed to set group attributes.'))
}): string[])
9 changes: 8 additions & 1 deletion frontend/controller/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ Vue.use(Router)
*/
const homeGuard = {
guard: (to, from) => !!store.state.currentGroupId,
redirect: (to, from) => ({ path: store.getters.ourProfileActive ? '/dashboard' : '/pending-approval' })
redirect: (to, from) => ({
path:
// If we haven't accepted the invite OR we haven't clicked 'Awesome' on
// the welcome screen, redirect to the '/pending-approval' page
store.getters.seenWelcomeScreen
? '/dashboard'
: '/pending-approval'
})
}

const loginGuard = {
Expand Down
31 changes: 28 additions & 3 deletions frontend/model/contracts/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { L } from '@common/common.js'
import sbp from '@sbp/sbp'
import { arrayOf, boolean, object, objectMaybeOf, objectOf, optional, string, stringMax, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'
import { arrayOf, boolean, object, objectMaybeOf, objectOf, optional, string, stringMax, unionOf, validatorFrom } from '~/frontend/model/contracts/misc/flowTyper.js'
import { LEFT_GROUP } from '~/frontend/utils/events.js'
import { Secret } from '~/shared/domains/chelonia/Secret.js'
import { findForeignKeysByContractID, findKeyIdByName } from '~/shared/domains/chelonia/utils.js'
Expand Down Expand Up @@ -283,8 +283,13 @@ sbp('chelonia/defineContract', {
throw new Error(`Cannot leave group ${groupContractID} because the reference hash does not match the latest`)
}

state.groups[groupContractID].hasLeft = true
delete state.groups[groupContractID].inviteSecret
// We only keep `hash` and `hasLeft` in the list of groups, as this
// is the only information we need for groups we're not part of.
// This has the advantage that we don't need to explicitly delete
// every new attribute that we may add in the future, but has the
// downside that, if we were to add a new attribute that's needed after
// having left, then it'd need to be added here.
state.groups[groupContractID] = { hash: reference, hasLeft: true }
corrideat marked this conversation as resolved.
Show resolved Hide resolved
},
sideEffect ({ data, contractID }) {
sbp('gi.contracts/identity/referenceTally', contractID, data.groupContractID, 'release')
Expand Down Expand Up @@ -359,6 +364,26 @@ sbp('chelonia/defineContract', {
delete state.fileDeleteTokens[manifestCid]
}
}
},
'gi.contracts/identity/setGroupAttributes': {
validate: objectOf({
groupContractID: string,
attributes: objectMaybeOf({
seenWelcomeScreen: validatorFrom((v) => v === true)
})
}),
process ({ data }, { state }) {
const { groupContractID, attributes } = data
if (!has(state.groups, groupContractID) || state.groups[groupContractID].hasLeft) {
throw new Error('Can\'t set attributes of groups you\'re not a member of')
}
if (attributes.seenWelcomeScreen) {
if (state.groups[groupContractID].seenWelcomeScreen) {
throw new Error('seenWelcomeScreen already set')
}
state.groups[groupContractID].seenWelcomeScreen = attributes.seenWelcomeScreen
}
}
}
},
methods: {
Expand Down
3 changes: 3 additions & 0 deletions frontend/model/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,9 @@ const getters = {
return nameA.normalize().toUpperCase() > nameB.normalize().toUpperCase() ? 1 : -1
})
},
seenWelcomeScreen (state, getters) {
return getters.ourProfileActive && getters.currentIdentityState?.groups?.[state.currentGroupId]?.seenWelcomeScreen
},
...chatroomGetters,
...groupGetters,
...identityGetters
Expand Down
14 changes: 12 additions & 2 deletions frontend/views/components/GroupWelcome.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
</template>

<script>
import { mapGetters } from 'vuex'
import sbp from '@sbp/sbp'
import { mapGetters, mapState } from 'vuex'
import Avatar from '@components/Avatar.vue'
import ConfettiAnimation from '@components/confetti-animation/ConfettiAnimation.vue'

Expand All @@ -41,7 +42,8 @@ export default ({
$v: { type: Object }
},
computed: {
...mapGetters(['groupSettings'])
...mapState(['currentGroupId']),
...mapGetters(['groupSettings', 'ourIdentityContractId'])
},
data () {
return {
Expand All @@ -52,6 +54,14 @@ export default ({
toDashboard () {
if (this.isButtonClicked) return
this.isButtonClicked = true
const groupContractID = this.currentGroupId
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering whether this should be here, or if instead it should be done on the /dashboard page on onMounted. Maybe it doesn't matter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pros of doing it here: it's only done once (for users). Pros of doing it in the /dashboard page: we don't need to change the commands.js in the tests. Cons of doing it in the /dashboard page: need to do this check every time.

sbp('gi.actions/identity/setGroupAttributes', {
contractID: this.ourIdentityContractId,
data: {
groupContractID,
attributes: { seenWelcomeScreen: true }
}
}).catch(e => console.warn('[GroupWelcome.vue] Error setting seenWelcomeScreen attribute', groupContractID, e))
this.$router.push({ path: '/dashboard' })
}
}
Expand Down
15 changes: 9 additions & 6 deletions frontend/views/pages/Home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ export default ({
},
computed: {
...mapGetters([
'ourProfileActive'
'currentIdentityState',
'seenWelcomeScreen'
]),
...mapState([
'currentGroupId'
Expand All @@ -103,11 +104,11 @@ export default ({
data () {
return {
ephemeral: {
ourProfileActive: false,
seenWelcomeScreen: false,
listener: ({ contractID }) => {
if (contractID !== this.currentGroupId) return
// For first time joins, force redirect to /pending-approval
this.ephemeral.ourProfileActive = false
this.ephemeral.seenWelcomeScreen = false
},
showPwaPromo: false
}
Expand All @@ -118,7 +119,7 @@ export default ({
this.checkPwaInstallability()
},
mounted () {
this.ephemeral.ourProfileActive = this.ourProfileActive
this.ephemeral.seenWelcomeScreen = this.seenWelcomeScreen
if (this.isLoggedIn && this.currentGroupId) {
this.navigateToGroupPage()
} else if (this.$route.query.next) {
Expand All @@ -140,7 +141,7 @@ export default ({
// In this particular condition, the app needs to immediately redirect user to '$route.query.next'
// so that the user stays in the same page after the browser refresh.
// (Related GH issue: https://github.com/okTurtles/group-income/issues/1830)
const path = this.$route.query.next ?? (this.ephemeral.ourProfileActive ? '/dashboard' : '/pending-approval')
const path = this.$route.query.next ?? (this.ephemeral.seenWelcomeScreen ? '/dashboard' : '/pending-approval')
this.$router.push({ path }).catch(e => ignoreWhenNavigationCancelled(e, path))
},
checkPwaInstallability () {
Expand All @@ -155,7 +156,9 @@ export default ({
},
watch: {
currentGroupId (to) {
this.ephemeral.ourProfileActive = this.ourProfileActive
// Redirect to `/pending-approval` if our profile isn't active or the
// welcome screen hasn't been approved
this.ephemeral.seenWelcomeScreen = this.seenWelcomeScreen
if (to && this.isLoggedIn) {
this.navigateToGroupPage()
}
Expand Down
19 changes: 16 additions & 3 deletions test/cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ import { CONTRACTS_MODIFIED_READY, EVENT_HANDLED_READY, EVENT_PUBLISHED, EVENT_P
const API_URL = Cypress.config('baseUrl')

// util funcs
const setGroupSeenWelcomeScreen = (sbp) => {
const state = sbp('state/vuex/state')
return sbp('gi.actions/identity/setGroupAttributes', {
contractID: state.loggedIn.identityContractID,
data: {
groupContractID: state.currentGroupId,
attributes: { seenWelcomeScreen: true }
}
})
}

const randomFromArray = arr => arr[Math.floor(Math.random() * arr.length)] // importing giLodash.js fails for some reason.
const getParamsFromInvitationLink = invitationLink => {
const params = new URLSearchParams(new URL(invitationLink).hash.slice(1))
Expand Down Expand Up @@ -231,7 +242,7 @@ Cypress.Commands.add('giLogin', (username, {
if (firstLoginAfterJoinGroup) {
const router = sbp('controller/router')
if (router.history.current.path === '/dashboard') return
return router.push({ path: '/dashboard' }) // .catch(() => {})
return setGroupSeenWelcomeScreen(sbp).then(() => router.push({ path: '/dashboard' })) // .catch(() => {})
}
})
})
Expand Down Expand Up @@ -331,7 +342,7 @@ Cypress.Commands.add('giCreateGroup', (name, {

const timeoutId = setTimeout(() => {
reject(new Error('[cypress] Timed out waiting for JOINED_GROUP event and active profile status'))
}, 5000)
}, 15000)

const cID = await sbp('gi.app/group/createAndSwitch', {
data: {
Expand All @@ -347,7 +358,7 @@ Cypress.Commands.add('giCreateGroup', (name, {
}).then(() => {
const router = sbp('controller/router')
if (router.history.current.path === '/dashboard') return
return router.push({ path: '/dashboard' })
return setGroupSeenWelcomeScreen(sbp).then(() => router.push({ path: '/dashboard' }))
})
})
cy.url().should('eq', `${API_URL}/app/dashboard`)
Expand Down Expand Up @@ -800,6 +811,8 @@ Cypress.Commands.add('giWaitUntilMessagesLoaded', (isGroupChannel = true) => {
})

Cypress.Commands.add('giSendMessage', (sender, message) => {
// The following is to ensure the chatroom has finished loading (no spinner)
cy.giWaitUntilMessagesLoaded(false)
cy.getByDT('messageInputWrapper').within(() => {
cy.get('textarea').type(`{selectall}{del}${message}{enter}`, { force: true })
cy.get('textarea').should('be.empty')
Expand Down