diff --git a/src/phpunit.xml b/src/phpunit.xml
index 7a316a86..6edec58c 100644
--- a/src/phpunit.xml
+++ b/src/phpunit.xml
@@ -1,64 +1,47 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <phpunit backupGlobals="false"
          backupStaticAttributes="false"
          bootstrap="vendor/autoload.php"
          colors="true"
          convertErrorsToExceptions="true"
          convertNoticesToExceptions="true"
          convertWarningsToExceptions="true"
          processIsolation="false"
          stopOnFailure="false">
     <testsuites>
         <testsuite name="Unit">
             <directory suffix="Test.php">tests/Unit</directory>
         </testsuite>
 
         <testsuite name="Functional">
             <directory suffix="Test.php">tests/Functional</directory>
         </testsuite>
 
         <testsuite name="Feature">
             <directory suffix="Test.php">tests/Feature</directory>
         </testsuite>
 
         <testsuite name="Browser">
             <directory suffix="Test.php">tests/Browser</directory>
-            <exclude>tests/Browser/Reseller/DashboardTest.php</exclude>
-            <exclude>tests/Browser/Reseller/DistlistTest.php</exclude>
-            <exclude>tests/Browser/Reseller/DomainTest.php</exclude>
-            <exclude>tests/Browser/Reseller/InvitationsTest.php</exclude>
-            <exclude>tests/Browser/Reseller/LogonTest.php</exclude>
-            <exclude>tests/Browser/Reseller/PaymentMollieTest.php</exclude>
-            <exclude>tests/Browser/Reseller/ResourceTest.php</exclude>
-            <exclude>tests/Browser/Reseller/SharedFolderTest.php</exclude>
-            <exclude>tests/Browser/Reseller/StatsTest.php</exclude>
-            <exclude>tests/Browser/Reseller/UserTest.php</exclude>
-            <exclude>tests/Browser/Reseller/WalletTest.php</exclude>
-            <exclude>tests/Browser/Reseller/UserFinancesTest.php</exclude>
-            <exclude>tests/Browser/LogonTest.php</exclude>
-            <exclude>tests/Browser/SignupTest.php</exclude>
             <exclude>tests/Browser/PaymentStripeTest.php</exclude>
-            <exclude>tests/Browser/Meet/RoomSetupTest.php</exclude>
-            <exclude>tests/Browser/Meet/RoomControlsTest.php</exclude>
-            <exclude>tests/Browser/Meet/RoomModeratorTest.php</exclude>
         </testsuite>
     </testsuites>
     <coverage processUncoveredFiles="true">
         <include>
             <directory suffix=".php">./app</directory>
         </include>
     </coverage>
     <logging>
         <testdoxHtml outputFile="./tests/report/testdox.html" />
     </logging>
     <php>
         <server name="APP_ENV" value="testing"/>
         <server name="APP_DEBUG" value="true"/>
         <server name="BCRYPT_ROUNDS" value="4"/>
         <server name="MAIL_MAILER" value="array"/>
         <server name="QUEUE_CONNECTION" value="sync"/>
         <server name="SESSION_DRIVER" value="array"/>
         <server name="SWOOLE_HTTP_ACCESS_LOG" value="false"/>
         <server name="PGP_LENGTH" value="1024"/>
     </php>
 </phpunit>
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
index 390d542f..4e857ba0 100644
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -1,455 +1,454 @@
 /**
  * First we will load all of this project's JavaScript dependencies which
  * includes Vue and other libraries. It is a great starting point when
  * building robust, powerful web applications using Vue and Laravel.
  */
 
 require('./bootstrap')
 
 import AppComponent from '../vue/App'
 import MenuComponent from '../vue/Widgets/Menu'
 import SupportForm from '../vue/Widgets/SupportForm'
-import { Tab } from 'bootstrap'
 import { loadLangAsync, i18n } from './locale'
 import { clearFormValidation, pick, startLoading, stopLoading } from './utils'
 
 const routerState = {
     afterLogin: null,
     isLoggedIn: !!localStorage.getItem('token')
 }
 
 let loadingRoute
 
 // Note: This has to be before the app is created
 // Note: You cannot use app inside of the function
 window.router.beforeEach((to, from, next) => {
     // check if the route requires authentication and user is not logged in
     if (to.meta.requiresAuth && !routerState.isLoggedIn) {
         // remember the original request, to use after login
         routerState.afterLogin = to;
 
         // redirect to login page
         next({ name: 'login' })
 
         return
     }
 
     if (to.meta.loading) {
         startLoading()
         loadingRoute = to.name
     }
 
     next()
 })
 
 window.router.afterEach((to, from) => {
     if (to.name && loadingRoute === to.name) {
         stopLoading()
         loadingRoute = null
     }
 
     // When changing a page remove old:
     // - error page
     // - modal backdrop
     $('#error-page,.modal-backdrop.show').remove()
     $('body').css('padding', 0) // remove padding added by unclosed modal
 
     // Close the mobile menu
     if ($('#header-menu .navbar-collapse.show').length) {
         $('#header-menu .navbar-toggler').click();
     }
 })
 
 const app = new Vue({
     components: {
         AppComponent,
         MenuComponent,
     },
     i18n,
     router: window.router,
     data() {
         return {
             authInfo: null,
             isUser: !window.isAdmin && !window.isReseller,
             appName: window.config['app.name'],
             appUrl: window.config['app.url'],
             themeDir: '/themes/' + window.config['app.theme']
         }
     },
     methods: {
         clearFormValidation,
         hasPermission(type) {
             const key = 'enable' + type.charAt(0).toUpperCase() + type.slice(1)
             return !!(this.authInfo && this.authInfo.statusInfo[key])
         },
         hasRoute(name) {
             return this.$router.resolve({ name: name }).resolved.matched.length > 0
         },
         hasSKU(name) {
             return this.authInfo.statusInfo.skus && this.authInfo.statusInfo.skus.indexOf(name) != -1
         },
         isController(wallet_id) {
             if (wallet_id && this.authInfo) {
                 let i
                 for (i = 0; i < this.authInfo.wallets.length; i++) {
                     if (wallet_id == this.authInfo.wallets[i].id) {
                         return true
                     }
                 }
                 for (i = 0; i < this.authInfo.accounts.length; i++) {
                     if (wallet_id == this.authInfo.accounts[i].id) {
                         return true
                     }
                 }
             }
 
             return false
         },
         isDegraded() {
             return this.authInfo && this.authInfo.isAccountDegraded
         },
         // Set user state to "logged in"
         loginUser(response, dashboard, update) {
             if (!update) {
                 routerState.isLoggedIn = true
                 this.authInfo = null
             }
 
             localStorage.setItem('token', response.access_token)
             localStorage.setItem('refreshToken', response.refresh_token)
             axios.defaults.headers.common.Authorization = 'Bearer ' + response.access_token
 
             if (response.email) {
                 this.authInfo = response
             }
 
             if (dashboard !== false) {
                 this.$router.push(routerState.afterLogin || { name: 'dashboard' })
             }
 
             routerState.afterLogin = null
 
             // Refresh the token before it expires
             let timeout = response.expires_in || 0
 
             // We'll refresh 60 seconds before the token expires
             if (timeout > 60) {
                 timeout -= 60
             }
 
             // TODO: We probably should try a few times in case of an error
             // TODO: We probably should prevent axios from doing any requests
             //       while the token is being refreshed
 
             this.refreshTimeout = setTimeout(() => {
                 axios.post('api/auth/refresh', { refresh_token: response.refresh_token }).then(response => {
                     this.loginUser(response.data, false, true)
                 })
             }, timeout * 1000)
         },
         // Set user state to "not logged in"
         logoutUser(redirect) {
             routerState.isLoggedIn = true
             this.authInfo = null
             localStorage.setItem('token', '')
             localStorage.setItem('refreshToken', '')
             delete axios.defaults.headers.common.Authorization
 
             if (redirect !== false) {
                 this.$router.push({ name: 'login' })
             }
 
             clearTimeout(this.refreshTimeout)
         },
         logo(mode) {
             let src = this.appUrl + this.themeDir + '/images/logo_' + (mode || 'header') + '.png'
 
             return `<img src="${src}" alt="${this.appName}">`
         },
         pick,
         startLoading,
         stopLoading,
-        tab(e) {
-            e.preventDefault()
-            new Tab(e.target).show()
-        },
         errorPage(code, msg, hint) {
             // Until https://github.com/vuejs/vue-router/issues/977 is implemented
             // we can't really use router to display error page as it has two side
             // effects: it changes the URL and adds the error page to browser history.
             // For now we'll be replacing current view with error page "manually".
 
             if (!msg) msg = this.$te('error.' + code) ? this.$t('error.' + code) : this.$t('error.unknown')
             if (!hint) hint = ''
 
             const error_page = '<div id="error-page" class="error-page">'
                 + `<div class="code">${code}</div><div class="message">${msg}</div><div class="hint">${hint}</div>`
                 + '</div>'
 
             $('#error-page').remove()
             $('#app').append(error_page)
 
             app.updateBodyClass('error')
         },
         errorHandler(error) {
             stopLoading()
 
             const status = error.response ? error.response.status : 500
             const message = error.response ? error.response.statusText : ''
 
             if (status == 401) {
                 // Remember requested route to come back to it after log in
                 if (this.$route.meta.requiresAuth) {
                     routerState.afterLogin = this.$route
                     this.logoutUser()
                 } else {
                     this.logoutUser(false)
                 }
             } else {
+                if (!error.response) {
+                    console.error(error)
+                }
+
                 this.errorPage(status, message)
             }
         },
         price(price, currency) {
             // TODO: Set locale argument according to the currently used locale
             return ((price || 0) / 100).toLocaleString('de-DE', { style: 'currency', currency: currency || 'CHF' })
         },
         priceLabel(cost, discount, currency) {
             let index = ''
 
             if (discount) {
                 cost = Math.floor(cost * ((100 - discount) / 100))
                 index = '\u00B9'
             }
 
             return this.price(cost, currency) + '/' + this.$t('wallet.month') + index
         },
         clickRecord(event) {
             if (!/^(a|button|svg|path)$/i.test(event.target.nodeName)) {
                 $(event.target).closest('tr').find('a').trigger('click')
             }
         },
         pageName(path) {
             let page = this.$route.path
 
             // check if it is a "menu page", find the page name
             // otherwise we'll use the real path as page name
             window.config.menu.every(item => {
                 if (item.location == page && item.page) {
                     page = item.page
                     return false
                 }
             })
 
             page = page.replace(/^\//, '')
 
             return page ? page : '404'
         },
         supportDialog(container) {
             let dialog = $('#support-dialog')[0]
 
             if (!dialog) {
                 // FIXME: Find a nicer way of doing this
                 SupportForm.i18n = i18n
                 let form = new Vue(SupportForm)
                 form.$mount($('<div>').appendTo(container)[0])
                 form.$root = this
                 form.$toast = this.$toast
                 dialog = form.$el
             }
 
             dialog.__vue__.show()
         },
         statusClass(obj) {
             if (obj.isDeleted) {
                 return 'text-muted'
             }
 
             if (obj.isDegraded || obj.isAccountDegraded || obj.isSuspended) {
                 return 'text-warning'
             }
 
             if (obj.isImapReady === false || obj.isLdapReady === false || obj.isVerified === false || obj.isConfirmed === false) {
                 return 'text-danger'
             }
 
             return 'text-success'
         },
         statusText(obj) {
             if (obj.isDeleted) {
                 return this.$t('status.deleted')
             }
 
             if (obj.isDegraded || obj.isAccountDegraded) {
                 return this.$t('status.degraded')
             }
 
             if (obj.isSuspended) {
                 return this.$t('status.suspended')
             }
 
             if (obj.isImapReady === false || obj.isLdapReady === false || obj.isVerified === false || obj.isConfirmed === false) {
                 return this.$t('status.notready')
             }
 
             return this.$t('status.active')
         },
         // Append some wallet properties to the object
         userWalletProps(object) {
             let wallet = this.authInfo.accounts[0]
 
             if (!wallet) {
                 wallet = this.authInfo.wallets[0]
             }
 
             if (wallet) {
                 object.currency = wallet.currency
                 if (wallet.discount) {
                     object.discount = wallet.discount
                     object.discount_description = wallet.discount_description
                 }
             }
         },
         updateBodyClass(name) {
             // Add 'class' attribute to the body, different for each page
             // so, we can apply page-specific styles
             document.body.className = 'page-' + (name || this.pageName()).replace(/\/.*$/, '')
         }
     }
 })
 
 // Fetch the locale file and the start the app
 loadLangAsync().then(() => app.$mount('#app'))
 
 // Add a axios request interceptor
 axios.interceptors.request.use(
     config => {
         // This is the only way I found to change configuration options
         // on a running application. We need this for browser testing.
         config.headers['X-Test-Payment-Provider'] = window.config.paymentProvider
 
         let loader = config.loader
         if (loader) {
             startLoading(loader)
         }
 
         return config
     },
     error => {
         // Do something with request error
         return Promise.reject(error)
     }
 )
 
 // Add a axios response interceptor for general/validation error handler
 axios.interceptors.response.use(
     response => {
         if (response.config.onFinish) {
             response.config.onFinish()
         }
 
         let loader = response.config.loader
         if (loader) {
             stopLoading(loader)
         }
 
         return response
     },
     error => {
         let loader = error.config.loader
         if (loader) {
             stopLoading(loader)
         }
 
         // Do not display the error in a toast message, pass the error as-is
         if (axios.isCancel(error) || error.config.ignoreErrors) {
             return Promise.reject(error)
         }
 
         if (error.config.onFinish) {
             error.config.onFinish()
         }
 
         let error_msg
 
         const status = error.response ? error.response.status : 200
         const data = error.response ? error.response.data : {}
 
         if (status == 422 && data.errors) {
             error_msg = app.$t('error.form')
 
             const modal = $('div.modal.show')
 
             $(modal.length ? modal : 'form').each((i, form) => {
                 form = $(form)
 
                 $.each(data.errors, (idx, msg) => {
                     const input_name = (form.data('validation-prefix') || form.find('form').first().data('validation-prefix') || '') + idx
                     let input = form.find('#' + input_name)
 
                     if (!input.length) {
                         input = form.find('[name="' + input_name + '"]');
                     }
 
                     if (input.length) {
                         // Create an error message
                         // API responses can use a string, array or object
                         let msg_text = ''
                         if (typeof(msg) !== 'string') {
                             $.each(msg, (index, str) => {
                                 msg_text += str + ' '
                             })
                         }
                         else {
                             msg_text = msg
                         }
 
                         let feedback = $('<div class="invalid-feedback">').text(msg_text)
 
                         if (input.is('.list-input')) {
                             // List input widget
                             let controls = input.children(':not(:first-child)')
 
                             if (!controls.length && typeof msg == 'string') {
                                 // this is an empty list (the main input only)
                                 // and the error message is not an array
                                 input.find('.main-input').addClass('is-invalid')
                             } else {
                                 controls.each((index, element) => {
                                     if (msg[index]) {
                                         $(element).find('input').addClass('is-invalid')
                                     }
                                 })
                             }
 
                             input.addClass('is-invalid').next('.invalid-feedback').remove()
                             input.after(feedback)
                         } else {
                             // a special case, e.g. the invitation policy widget
                             if (input.is('select') && input.parent().is('.input-group-select.selected')) {
                                 input = input.next()
                             }
 
                             // Standard form element
                             input.addClass('is-invalid')
                             input.parent().find('.invalid-feedback').remove()
                             input.parent().append(feedback)
                         }
                     }
                 })
 
                 form.find('.is-invalid:not(.list-input)').first().focus()
             })
         }
         else if (data.status == 'error') {
             error_msg = data.message
         }
         else {
             error_msg = error.request ? error.request.statusText : error.message
         }
 
         app.$toast.error(error_msg || app.$t('error.server'))
 
         // Pass the error as-is
         return Promise.reject(error)
     }
 )
diff --git a/src/resources/js/bootstrap.js b/src/resources/js/bootstrap.js
index a4436d72..0647aa15 100644
--- a/src/resources/js/bootstrap.js
+++ b/src/resources/js/bootstrap.js
@@ -1,105 +1,106 @@
 /**
  * Import Cash (jQuery replacement)
  */
 
 import $ from 'cash-dom'
 window.$ = $
 
 $.fn.focus = function() {
     if (this.length && this[0].focus) {
         this[0].focus()
     }
     return this
 }
 
 $.fn.click = function() {
     if (this.length && this[0].click) {
         this[0].click()
     }
     return this
 }
 
 /**
  * Load Vue, VueRouter and global components
  */
 
 import Vue from 'vue'
 import VueRouter from 'vue-router'
 import Btn from '../vue/Widgets/Btn'
 import BtnRouter from '../vue/Widgets/BtnRouter'
+import Tabs from '../vue/Widgets/Tabs'
 import Toast from '../vue/Widgets/Toast'
 import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
 import { Tooltip } from 'bootstrap'
 
 window.Vue = Vue
 
-Vue.component('SvgIcon', FontAwesomeIcon)
 Vue.component('Btn', Btn)
 Vue.component('BtnRouter', BtnRouter)
+Vue.component('SvgIcon', FontAwesomeIcon)
+Vue.component('Tabs', Tabs)
 
 const vTooltip = (el, binding) => {
     let t = []
 
     if (binding.modifiers.focus) t.push('focus')
     if (binding.modifiers.hover) t.push('hover')
     if (binding.modifiers.click) t.push('click')
     if (!t.length) t.push('click')
 
     el.tooltip = new Tooltip(el, {
         title: binding.value,
         placement: binding.arg || 'top',
         trigger: t.join(' '),
         html: !!binding.modifiers.html
     })
 }
 
 Vue.directive('tooltip', {
     bind: vTooltip,
     update: vTooltip,
     unbind (el) {
         el.tooltip.dispose()
     }
 })
 
 Vue.use(Toast)
-
 Vue.use(VueRouter)
 
 let vueRouterBase = '/'
 try {
   let url = new URL(window.config['app.url'])
   vueRouterBase = url.pathname
 } catch(e) {
     // ignore
 }
 
 window.router = new VueRouter({
     base: vueRouterBase,
     mode: 'history',
     routes: window.routes,
     scrollBehavior (to, from, savedPosition) {
         // Scroll the page to top, but not on Back action
         return savedPosition || { x: 0, y: 0 }
     }
 })
 
 /**
  * Load the axios HTTP library which allows us to easily issue requests
  * to our Laravel back-end. This library automatically handles sending the
  * CSRF token as a header based on the value of the "XSRF" token cookie.
  */
 
 window.axios = require('axios')
 axios.defaults.baseURL = vueRouterBase
 axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
 
 // Register a few most common icons
 import { library } from '@fortawesome/fontawesome-svg-core'
 library.add(
     require('@fortawesome/free-solid-svg-icons/faCheck').definition,
     require('@fortawesome/free-solid-svg-icons/faCircleInfo').definition,
     require('@fortawesome/free-solid-svg-icons/faPlus').definition,
     require('@fortawesome/free-solid-svg-icons/faMagnifyingGlass').definition,
     require('@fortawesome/free-solid-svg-icons/faTrashCan').definition,
     require('@fortawesome/free-solid-svg-icons/faUser').definition,
 )
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
index 2784bf13..0cb827ad 100644
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -1,534 +1,534 @@
 <?php
 
 /**
  * This file will be converted to a Vue-i18n compatible JSON format on build time
  *
  * Note: The Laravel localization features do not work here. Vue-i18n rules are different
  */
 
 return [
 
     'app' => [
         'faq' => "FAQ",
     ],
 
     'btn' => [
         'add' => "Add",
         'accept' => "Accept",
         'back' => "Back",
         'cancel' => "Cancel",
         'close' => "Close",
         'continue' => "Continue",
         'copy' => "Copy",
         'delete' => "Delete",
         'deny' => "Deny",
         'download' => "Download",
         'edit' => "Edit",
         'file' => "Choose file...",
         'moreinfo' => "More information",
         'refresh' => "Refresh",
         'reset' => "Reset",
         'resend' => "Resend",
         'save' => "Save",
         'search' => "Search",
         'share' => "Share",
         'signup' => "Sign Up",
         'submit' => "Submit",
         'suspend' => "Suspend",
         'unsuspend' => "Unsuspend",
         'verify' => "Verify",
     ],
 
     'companion' => [
         'title' => "Companion App",
         'name' => "Name",
         'description' => "Use the Companion App on your mobile phone for advanced two factor authentication.",
         'pair-new' => "Pair new device",
         'paired' => "Paired devices",
         'pairing-instructions' => "Pair a new device using the following QR-Code:",
         'deviceid' => "Device ID",
         'list-empty' => "There are currently no devices",
         'delete' => "Remove devices",
         'remove-devices' => "Remove Devices",
         'remove-devices-text' => "Do you really want to remove all devices permanently?"
             . " Please note that this action cannot be undone, and you can only remove all devices together."
             . " You may pair devices you would like to keep individually again.",
     ],
 
     'dashboard' => [
         'beta' => "beta",
         'distlists' => "Distribution lists",
         'chat' => "Video chat",
         'companion' => "Companion app",
         'domains' => "Domains",
         'files' => "Files",
         'invitations' => "Invitations",
         'profile' => "Your profile",
         'resources' => "Resources",
         'settings' => "Settings",
         'shared-folders' => "Shared folders",
         'users' => "User accounts",
         'wallet' => "Wallet",
         'webmail' => "Webmail",
         'stats' => "Stats",
     ],
 
     'distlist' => [
         'list-title' => "Distribution list | Distribution lists",
         'create' => "Create list",
         'delete' => "Delete list",
         'email' => "Email",
         'list-empty' => "There are no distribution lists in this account.",
         'name' => "Name",
         'new' => "New distribution list",
         'recipients' => "Recipients",
         'sender-policy' => "Sender Access List",
         'sender-policy-text' => "With this list you can specify who can send mail to the distribution list."
             . " You can put a complete email address (jane@kolab.org), domain (kolab.org) or suffix (.org) that the sender email address is compared to."
             . " If the list is empty, mail from anyone is allowed.",
     ],
 
     'domain' => [
         'delete' => "Delete domain",
         'delete-domain' => "Delete {domain}",
         'delete-text' => "Do you really want to delete this domain permanently?"
             . " This is only possible if there are no users, aliases or other objects in this domain."
             . " Please note that this action cannot be undone.",
         'dns-verify' => "Domain DNS verification sample:",
         'dns-config' => "Domain DNS configuration sample:",
         'list-empty' => "There are no domains in this account.",
         'namespace' => "Namespace",
         'spf-whitelist' => "SPF Whitelist",
         'spf-whitelist-text' => "The Sender Policy Framework allows a sender domain to disclose, through DNS, "
             . "which systems are allowed to send emails with an envelope sender address within said domain.",
         'spf-whitelist-ex' => "Here you can specify a list of allowed servers, for example: <var>.ess.barracuda.com</var>.",
         'verify' => "Domain verification",
         'verify-intro' => "In order to confirm that you're the actual holder of the domain, we need to run a verification process before finally activating it for email delivery.",
         'verify-dns' => "The domain <b>must have one of the following entries</b> in DNS:",
         'verify-dns-txt' => "TXT entry with value:",
         'verify-dns-cname' => "or CNAME entry:",
         'verify-outro' => "When this is done press the button below to start the verification.",
         'verify-sample' => "Here's a sample zone file for your domain:",
         'config' => "Domain configuration",
         'config-intro' => "In order to let {app} receive email traffic for your domain you need to adjust the DNS settings, more precisely the MX entries, accordingly.",
         'config-sample' => "Edit your domain's zone file and replace existing MX entries with the following values:",
         'config-hint' => "If you don't know how to set DNS entries for your domain, please contact the registration service where you registered the domain or your web hosting provider.",
         'create' => "Create domain",
         'new' => "New domain",
     ],
 
     'error' => [
         '400' => "Bad request",
         '401' => "Unauthorized",
         '403' => "Access denied",
         '404' => "Not found",
         '405' => "Method not allowed",
         '500' => "Internal server error",
         'unknown' => "Unknown Error",
         'server' => "Server Error",
         'form' => "Form validation error",
     ],
 
     'file' => [
         'create' => "Create file",
         'delete' => "Delete file",
         'list-empty' => "There are no files in this account.",
         'mimetype' => "Mimetype",
         'mtime' => "Modified",
         'new' => "New file",
         'search' => "File name",
         'sharing' => "Sharing",
         'sharing-links-text' => "You can share the file with other users by giving them read-only access "
             . "to the file via a unique link.",
     ],
 
     'form' => [
         'acl' => "Access rights",
         'acl-full' => "All",
         'acl-read-only' => "Read-only",
         'acl-read-write' => "Read-write",
         'amount' => "Amount",
         'anyone' => "Anyone",
         'code' => "Confirmation Code",
         'config' => "Configuration",
         'date' => "Date",
         'description' => "Description",
         'details' => "Details",
         'disabled' => "disabled",
         'domain' => "Domain",
         'email' => "Email Address",
         'emails' => "Email Addresses",
         'enabled' => "enabled",
         'firstname' => "First Name",
         'general' => "General",
         'lastname' => "Last Name",
         'name' => "Name",
         'months' => "months",
         'none' => "none",
         'or' => "or",
         'password' => "Password",
         'password-confirm' => "Confirm Password",
         'phone' => "Phone",
         'settings' => "Settings",
         'shared-folder' => "Shared Folder",
         'size' => "Size",
         'status' => "Status",
         'surname' => "Surname",
         'type' => "Type",
         'user' => "User",
         'primary-email' => "Primary Email",
         'id' => "ID",
         'created' => "Created",
         'deleted' => "Deleted",
     ],
 
     'invitation' => [
         'create' => "Create invite(s)",
         'create-title' => "Invite for a signup",
         'create-email' => "Enter an email address of the person you want to invite.",
         'create-csv' => "To send multiple invitations at once, provide a CSV (comma separated) file, or alternatively a plain-text file, containing one email address per line.",
         'list-empty' => "There are no invitations in the database.",
         'title' => "Signup invitations",
         'search' => "Email address or domain",
         'send' => "Send invite(s)",
         'status-completed' => "User signed up",
         'status-failed' => "Sending failed",
         'status-sent' => "Sent",
         'status-new' => "Not sent yet",
     ],
 
     'lang' => [
         'en' => "English",
         'de' => "German",
         'fr' => "French",
         'it' => "Italian",
     ],
 
     'login' => [
         '2fa' => "Second factor code",
         '2fa_desc' => "Second factor code is optional for users with no 2-Factor Authentication setup.",
         'forgot_password' => "Forgot password?",
         'header' => "Please sign in",
         'sign_in' => "Sign in",
         'webmail' => "Webmail"
     ],
 
     'meet' => [
         'title' => "Voice & Video Conferencing",
         'welcome' => "Welcome to our beta program for Voice & Video Conferencing.",
         'url' => "You have a room of your own at the URL below. This room is only open when you yourself are in attendance. Use this URL to invite people to join you.",
         'notice' => "This is a work in progress and more features will be added over time. Current features include:",
         'sharing' => "Screen Sharing",
         'sharing-text' => "Share your screen for presentations or show-and-tell.",
         'security' => "Room Security",
         'security-text' => "Increase the room security by setting a password that attendees will need to know"
             . " before they can enter, or lock the door so attendees will have to knock, and a moderator can accept or deny those requests.",
         'qa-title' => "Raise Hand (Q&A)",
         'qa-text' => "Silent audience members can raise their hand to facilitate a Question & Answer session with the panel members.",
         'moderation' => "Moderator Delegation",
         'moderation-text' => "Delegate moderator authority for the session, so that a speaker is not needlessly"
             . " interrupted with attendees knocking and other moderator duties.",
         'eject' => "Eject Attendees",
         'eject-text' => "Eject attendees from the session in order to force them to reconnect, or address policy"
             . " violations. Click the user icon for effective dismissal.",
         'silent' => "Silent Audience Members",
         'silent-text' => "For a webinar-style session, configure the room to force all new attendees to be silent audience members.",
         'interpreters' => "Language Specific Audio Channels",
         'interpreters-text' => "Designate a participant to interpret the original audio to a target language, for sessions"
             . " with multi-lingual attendees. The interpreter is expected to be able to relay the original audio, and override it.",
         'beta-notice' => "Keep in mind that this is still in beta and might come with some issues."
             . " Should you encounter any on your way, let us know by contacting support.",
 
         // Room options dialog
         'options' => "Room options",
         'password' => "Password",
         'password-none' => "none",
         'password-clear' => "Clear password",
         'password-set' => "Set password",
         'password-text' => "You can add a password to your meeting. Participants will have to provide the password before they are allowed to join the meeting.",
         'lock' => "Locked room",
         'lock-text' => "When the room is locked participants have to be approved by a moderator before they could join the meeting.",
         'nomedia' => "Subscribers only",
         'nomedia-text' => "Forces all participants to join as subscribers (with camera and microphone turned off)."
             . " Moderators will be able to promote them to publishers throughout the session.",
 
         // Room menu
         'partcnt' => "Number of participants",
         'menu-audio-mute' => "Mute audio",
         'menu-audio-unmute' => "Unmute audio",
         'menu-video-mute' => "Mute video",
         'menu-video-unmute' => "Unmute video",
         'menu-screen' => "Share screen",
         'menu-hand-lower' => "Lower hand",
         'menu-hand-raise' => "Raise hand",
         'menu-channel' => "Interpreted language channel",
         'menu-chat' => "Chat",
         'menu-fullscreen' => "Full screen",
         'menu-fullscreen-exit' => "Exit full screen",
         'menu-leave' => "Leave session",
 
         // Room setup screen
         'setup-title' => "Set up your session",
         'mic' => "Microphone",
         'cam' => "Camera",
         'nick' => "Nickname",
         'nick-placeholder' => "Your name",
         'join' => "JOIN",
         'joinnow' => "JOIN NOW",
         'imaowner' => "I'm the owner",
 
         // Room
         'qa' => "Q & A",
         'leave-title' => "Room closed",
         'leave-body' => "The session has been closed by the room owner.",
         'media-title' => "Media setup",
         'join-request' => "Join request",
         'join-requested' => "{user} requested to join.",
 
         // Status messages
         'status-init' => "Checking the room...",
         'status-323' => "The room is closed. Please, wait for the owner to start the session.",
         'status-324' => "The room is closed. It will be open for others after you join.",
         'status-325' => "The room is ready. Please, provide a valid password.",
         'status-326' => "The room is locked. Please, enter your name and try again.",
         'status-327' => "Waiting for permission to join the room.",
         'status-404' => "The room does not exist.",
         'status-429' => "Too many requests. Please, wait.",
         'status-500' => "Failed to connect to the room. Server error.",
 
         // Other menus
         'media-setup' => "Media setup",
         'perm' => "Permissions",
         'perm-av' => "Audio &amp; Video publishing",
         'perm-mod' => "Moderation",
         'lang-int' => "Language interpreter",
         'menu-options' => "Options",
     ],
 
     'menu' => [
         'cockpit' => "Cockpit",
         'login' => "Login",
         'logout' => "Logout",
         'signup' => "Signup",
         'toggle' => "Toggle navigation",
     ],
 
     'msg' => [
         'initializing' => "Initializing...",
         'loading' => "Loading...",
         'loading-failed' => "Failed to load data.",
         'notfound' => "Resource not found.",
         'info' => "Information",
         'error' => "Error",
         'uploading' => "Uploading...",
         'warning' => "Warning",
         'success' => "Success",
     ],
 
     'nav' => [
         'more' => "Load more",
         'step' => "Step {i}/{n}",
     ],
 
     'password' => [
         'link-invalid' => "The password reset code is expired or invalid.",
         'reset' => "Password Reset",
         'reset-step1' => "Enter your email address to reset your password.",
         'reset-step1-hint' => "You may need to check your spam folder or unblock {email}.",
         'reset-step2' => "We sent out a confirmation code to your external email address."
             . " Enter the code we sent you, or click the link in the message.",
     ],
 
     'resource' => [
         'create' => "Create resource",
         'delete' => "Delete resource",
         'invitation-policy' => "Invitation policy",
         'invitation-policy-text' => "Event invitations for a resource are normally accepted automatically"
             . " if there is no conflicting event on the requested time slot. Invitation policy allows"
             . " for rejecting such requests or to require a manual acceptance from a specified user.",
         'ipolicy-manual' => "Manual (tentative)",
         'ipolicy-accept' => "Accept",
         'ipolicy-reject' => "Reject",
         'list-title' => "Resource | Resources",
         'list-empty' => "There are no resources in this account.",
         'new' => "New resource",
     ],
 
     'settings' => [
         'password-policy' => "Password Policy",
         'password-retention' => "Password Retention",
         'password-max-age' => "Require a password change every",
     ],
 
     'shf' => [
         'aliases-none' => "This shared folder has no email aliases.",
         'create' => "Create folder",
         'delete' => "Delete folder",
         'acl-text' => "Defines user permissions to access the shared folder.",
         'list-title' => "Shared folder | Shared folders",
         'list-empty' => "There are no shared folders in this account.",
         'new' => "New shared folder",
         'type-mail' => "Mail",
         'type-event' => "Calendar",
         'type-contact' => "Address Book",
         'type-task' => "Tasks",
         'type-note' => "Notes",
         'type-file' => "Files",
     ],
 
     'signup' => [
         'email' => "Existing Email Address",
         'login' => "Login",
         'title' => "Sign Up",
         'step1' => "Sign up to start your free month.",
         'step2' => "We sent out a confirmation code to your email address. Enter the code we sent you, or click the link in the message.",
         'step3' => "Create your Kolab identity (you can choose additional addresses later).",
         'voucher' => "Voucher Code",
     ],
 
     'status' => [
         'prepare-account' => "We are preparing your account.",
         'prepare-domain' => "We are preparing the domain.",
         'prepare-distlist' => "We are preparing the distribution list.",
         'prepare-resource' => "We are preparing the resource.",
         'prepare-shared-folder' => "We are preparing the shared folder.",
         'prepare-user' => "We are preparing the user account.",
         'prepare-hint' => "Some features may be missing or readonly at the moment.",
         'prepare-refresh' => "The process never ends? Press the \"Refresh\" button, please.",
         'ready-account' => "Your account is almost ready.",
         'ready-domain' => "The domain is almost ready.",
         'ready-distlist' => "The distribution list is almost ready.",
         'ready-resource' => "The resource is almost ready.",
         'ready-shared-folder' => "The shared-folder is almost ready.",
         'ready-user' => "The user account is almost ready.",
         'verify' => "Verify your domain to finish the setup process.",
         'verify-domain' => "Verify domain",
         'degraded' => "Degraded",
         'deleted' => "Deleted",
         'suspended' => "Suspended",
         'notready' => "Not Ready",
         'active' => "Active",
     ],
 
     'support' => [
         'title' => "Contact Support",
         'id' => "Customer number or email address you have with us",
         'id-pl' => "e.g. 12345678 or john@kolab.org",
         'id-hint' => "Leave blank if you are not a customer yet",
         'name' => "Name",
         'name-pl' => "how we should call you in our reply",
         'email' => "Working email address",
         'email-pl' => "make sure we can reach you at this address",
         'summary' => "Issue Summary",
         'summary-pl' => "one sentence that summarizes your issue",
         'expl' => "Issue Explanation",
     ],
 
     'user' => [
         '2fa-hint1' => "This will remove 2-Factor Authentication entitlement as well as the user-configured factors.",
         '2fa-hint2' => "Please, make sure to confirm the user identity properly.",
         'add-beta' => "Enable beta program",
         'address' => "Address",
         'aliases' => "Aliases",
-        'aliases-email' => "Email Aliases",
         'aliases-none' => "This user has no email aliases.",
         'add-bonus' => "Add bonus",
         'add-bonus-title' => "Add a bonus to the wallet",
         'add-penalty' => "Add penalty",
         'add-penalty-title' => "Add a penalty to the wallet",
         'auto-payment' => "Auto-payment",
         'auto-payment-text' => "Fill up by <b>{amount}</b> when under <b>{balance}</b> using {method}",
         'country' => "Country",
         'create' => "Create user",
         'custno' => "Customer No.",
         'degraded-warning' => "The account is degraded. Some features have been disabled.",
         'degraded-hint' => "Please, make a payment.",
         'delete' => "Delete user",
         'delete-account' => "Delete this account?",
         'delete-email' => "Delete {email}",
         'delete-text' => "Do you really want to delete this user permanently?"
             . " This will delete all account data and withdraw the permission to access the email account."
             . " Please note that this action cannot be undone.",
         'discount' => "Discount",
         'discount-hint' => "applied discount",
         'discount-title' => "Account discount",
         'distlists' => "Distribution lists",
         'domains' => "Domains",
         'ext-email' => "External Email",
+        'email-aliases' => "Email Aliases",
         'finances' => "Finances",
         'greylisting' => "Greylisting",
         'greylisting-text' => "Greylisting is a method of defending users against spam. Any incoming mail from an unrecognized sender "
             . "is temporarily rejected. The originating server should try again after a delay. "
             . "This time the email will be accepted. Spammers usually do not reattempt mail delivery.",
         'list-title' => "User accounts",
         'list-empty' => "There are no users in this account.",
         'managed-by' => "Managed by",
         'new' => "New user account",
         'org' => "Organization",
         'package' => "Package",
         'pass-input' => "Enter password",
         'pass-link' => "Set via link",
         'pass-link-label' => "Link:",
         'pass-link-hint' => "Press Submit to activate the link",
         'passwordpolicy' => "Password Policy",
         'price' => "Price",
         'profile-title' => "Your profile",
         'profile-delete' => "Delete account",
         'profile-delete-title' => "Delete this account?",
         'profile-delete-text1' => "This will delete the account as well as all domains, users and aliases associated with this account.",
         'profile-delete-warning' => "This operation is irreversible",
         'profile-delete-text2' => "As you will not be able to recover anything after this point, please make sure that you have migrated all data before proceeding.",
         'profile-delete-support' => "As we always strive to improve, we would like to ask for 2 minutes of your time. "
             . "The best tool for improvement is feedback from users, and we would like to ask "
             . "for a few words about your reasons for leaving our service. Please send your feedback to <a href=\"{href}\">{email}</a>.",
         'profile-delete-contact' => "Also feel free to contact {app} Support with any questions or concerns that you may have in this context.",
         'reset-2fa' => "Reset 2-Factor Auth",
         'reset-2fa-title' => "2-Factor Authentication Reset",
         'resources' => "Resources",
         'title' => "User account",
         'search' => "User email address or name",
         'search-pl' => "User ID, email or domain",
         'skureq' => "{sku} requires {list}.",
         'subscription' => "Subscription",
         'subscriptions' => "Subscriptions",
         'subscriptions-none' => "This user has no subscriptions.",
         'users' => "Users",
     ],
 
     'wallet' => [
         'add-credit' => "Add credit",
         'auto-payment-cancel' => "Cancel auto-payment",
         'auto-payment-change' => "Change auto-payment",
         'auto-payment-failed' => "The setup of automatic payments failed. Restart the process to enable automatic top-ups.",
         'auto-payment-hint' => "Here is how it works: Every time your account runs low, we will charge your preferred payment method for an amount you choose."
             . " You can cancel or change the auto-payment option at any time.",
         'auto-payment-setup' => "Set up auto-payment",
         'auto-payment-disabled' => "The configured auto-payment has been disabled. Top up your wallet or raise the auto-payment amount.",
         'auto-payment-info' => "Auto-payment is <b>set</b> to fill up your account by <b>{amount}</b> every time your account balance gets under <b>{balance}</b>.",
         'auto-payment-inprogress' => "The setup of the automatic payment is still in progress.",
         'auto-payment-next' => "Next, you will be redirected to the checkout page, where you can provide your credit card details.",
         'auto-payment-disabled-next' => "The auto-payment is disabled. Immediately after you submit new settings we'll enable it and attempt to top up your wallet.",
         'auto-payment-update' => "Update auto-payment",
         'banktransfer-hint' => "Please note that a bank transfer can take several days to complete.",
         'currency-conv' => "Here is how it works: You specify the amount by which you want to top up your wallet in {wc}."
             . " We will then convert this to {pc}, and on the next page you will be provided with the bank-details to transfer the amount in {pc}.",
         'fill-up' => "Fill up by",
         'history' => "History",
         'month' => "month",
         'noperm' => "Only account owners can access a wallet.",
         'payment-amount-hint' => "Choose the amount by which you want to top up your wallet.",
         'payment-method' => "Method of payment: {method}",
         'payment-warning' => "You will be charged for {price}.",
         'pending-payments' => "Pending Payments",
         'pending-payments-warning' => "You have payments that are still in progress. See the \"Pending Payments\" tab below.",
         'pending-payments-none' => "There are no pending payments for this account.",
         'receipts' => "Receipts",
         'receipts-hint' => "Here you can download receipts (in PDF format) for payments in specified period. Select the period and press the Download button.",
         'receipts-none' => "There are no receipts for payments in this account. Please, note that you can download receipts after the month ends.",
         'title' => "Account balance",
         'top-up' => "Top up your wallet",
         'transactions' => "Transactions",
         'transactions-none' => "There are no transactions for this account.",
         'when-below' => "when account balance is below",
     ],
 ];
diff --git a/src/resources/lang/fr/ui.php b/src/resources/lang/fr/ui.php
index 74ba9a51..2a6db372 100644
--- a/src/resources/lang/fr/ui.php
+++ b/src/resources/lang/fr/ui.php
@@ -1,482 +1,482 @@
 <?php
 
 /**
  * This file will be converted to a Vue-i18n compatible JSON format on build time
  *
  * Note: The Laravel localization features do not work here. Vue-i18n rules are different
  */
 
 return [
 
     'app' => [
         'faq' => "FAQ",
     ],
 
     'btn' => [
         'add' => "Ajouter",
         'accept' => "Accepter",
         'back' => "Back",
         'cancel' => "Annuler",
         'close' => "Fermer",
         'continue' => "Continuer",
         'delete' => "Supprimer",
         'deny' => "Refuser",
         'download' => "Télécharger",
         'edit' => "Modifier",
         'file' => "Choisir le ficher...",
         'moreinfo' => "Plus d'information",
         'refresh' => "Actualiser",
         'reset' => "Réinitialiser",
         'resend' => "Envoyer à nouveau",
         'save' => "Sauvegarder",
         'search' => "Chercher",
         'signup' => "S'inscrire",
         'submit' => "Soumettre",
         'suspend' => "Suspendre",
         'unsuspend' => "Débloquer",
         'verify' => "Vérifier",
     ],
 
     'dashboard' => [
         'beta' => "bêta",
         'distlists' => "Listes de distribution",
         'chat' => "Chat Vidéo",
         'domains' => "Domaines",
         'invitations' => "Invitations",
         'profile' => "Votre profil",
         'resources' => "Ressources",
         'users' => "D'utilisateurs",
         'wallet' => "Portefeuille",
         'webmail' => "Webmail",
         'stats' => "Statistiques",
     ],
 
     'distlist' => [
         'list-title' => "Liste de distribution | Listes de Distribution",
         'create' => "Créer une liste",
         'delete' => "Suprimmer une list",
         'email' => "Courriel",
         'list-empty' => "il n'y a pas de listes de distribution dans ce compte.",
         'name' => "Nom",
         'new' => "Nouvelle liste de distribution",
         'recipients' => "Destinataires",
         'sender-policy' => "Liste d'Accès d'Expéditeur",
         'sender-policy-text' => "Cette liste vous permet de spécifier qui peut envoyer du courrier à la liste de distribution."
             . " Vous pouvez mettre une adresse e-mail complète (jane@kolab.org), un domaine (kolab.org) ou un suffixe (.org)"
             . " auquel l'adresse électronique de l'expéditeur est assimilée."
             . " Si la liste est vide, le courriels de quiconque est autorisé."
     ],
 
     'domain' => [
         'dns-verify' => "Exemple de vérification du DNS d'un domaine:",
         'dns-config' => "Exemple de configuration du DNS d'un domaine:",
         'list-empty' => "Il y a pas de domaines dans ce compte.",
         'namespace' => "Espace de noms",
         'verify' => "Vérification du domaine",
         'verify-intro' => "Afin de confirmer que vous êtes bien le titulaire du domaine, nous devons exécuter un processus de vérification avant de l'activer définitivement pour la livraison d'e-mails.",
         'verify-dns' => "Le domaine <b>doit avoir l'une des entrées suivantes</b> dans le DNS:",
         'verify-dns-txt' => "Entrée TXT avec valeur:",
         'verify-dns-cname' => "ou entrée CNAME:",
         'verify-outro' => "Lorsque cela est fait, appuyez sur le bouton ci-dessous pour lancer la vérification.",
         'verify-sample' => "Voici un fichier de zone simple pour votre domaine:",
         'config' => "Configuration du domaine",
         'config-intro' => "Afin de permettre à {app} de recevoir le trafic de messagerie pour votre domaine, vous devez ajuster les paramètres DNS, plus précisément les entrées MX, en conséquence.",
         'config-sample' => "Modifiez le fichier de zone de votre domaine et remplacez les entrées MX existantes par les valeurs suivantes:",
         'config-hint' => "Si vous ne savez pas comment définir les entrées DNS pour votre domaine, veuillez contacter le service d'enregistrement auprès duquel vous avez enregistré le domaine ou votre fournisseur d'hébergement Web.",
         'spf-whitelist' => "SPF Whitelist",
         'spf-whitelist-text' => "Le Sender Policy Framework permet à un domaine expéditeur de dévoiler, par le biais de DNS,"
             . " quels systèmes sont autorisés à envoyer des e-mails avec une adresse d'expéditeur d'enveloppe dans le domaine en question.",
         'spf-whitelist-ex' => "Vous pouvez ici spécifier une liste de serveurs autorisés, par exemple: <var>.ess.barracuda.com</var>.",
         'create' => "Créer domaine",
         'new' => "Nouveau domaine",
         'delete' => "Supprimer domaine",
         'delete-domain' => "Supprimer {domain}",
         'delete-text' => "Voulez-vous vraiment supprimer ce domaine de façon permanente?"
             . " Ceci n'est possible que s'il n'y a pas d'utilisateurs, d'alias ou d'autres objets dans ce domaine."
             . " Veuillez noter que cette action ne peut pas être inversée.",
     ],
 
     'error' => [
         '400' => "Mauvaide demande",
         '401' => "Non autorisé",
         '403' => "Accès refusé",
         '404' => "Pas trouvé",
         '405' => "Méthode non autorisée",
         '500' => "Erreur de serveur interne",
         'unknown' => "Erreur inconnu",
         'server' => "Erreur de serveur",
         'form' => "Erreur de validation du formulaire",
     ],
 
     'form' => [
         'acl' => "Droits d'accès",
         'acl-full' => "Tout",
         'acl-read-only' => "Lecture seulement",
         'acl-read-write' => "Lecture-écriture",
         'amount' => "Montant",
         'anyone' => "Chacun",
         'code' => "Le code de confirmation",
         'config' => "Configuration",
         'date' => "Date",
         'description' => "Description",
         'details' => "Détails",
         'domain' => "Domaine",
         'email' => "Adresse e-mail",
         'firstname' => "Prénom",
         'lastname' => "Nom de famille",
         'none' => "aucun",
         'or' => "ou",
         'password' => "Mot de passe",
         'password-confirm' => "Confirmer le mot de passe",
         'phone' => "Téléphone",
         'shared-folder' => "Dossier partagé",
         'status' => "État",
         'surname' => "Nom de famille",
         'type' => "Type",
         'user' => "Utilisateur",
         'primary-email' => "Email principal",
         'id' => "ID",
         'created' => "Créé",
         'deleted' => "Supprimé",
         'disabled' => "Désactivé",
         'enabled' => "Activé",
         'general' => "Général",
         'settings' => "Paramètres",
     ],
 
     'invitation' => [
         'create' => "Créez des invitation(s)",
         'create-title' => "Invitation à une inscription",
         'create-email' => "Saisissez l'adresse électronique de la personne que vous souhaitez inviter.",
         'create-csv' => "Pour envoyer plusieurs invitations à la fois, fournissez un fichier CSV (séparé par des virgules) ou un fichier en texte brut, contenant une adresse e-mail par ligne.",
         'list-empty' => "Il y a aucune invitation dans la mémoire de données.",
         'title' => "Invitation d'inscription",
         'search' => "Adresse E-mail ou domaine",
         'send' => "Envoyer invitation(s)",
         'status-completed' => "Utilisateur s'est inscrit",
         'status-failed' => "L'envoi a échoué",
         'status-sent' => "Envoyé",
         'status-new' => "Pas encore envoyé",
     ],
 
     'lang' => [
         'en' => "Anglais",
         'de' => "Allemand",
         'fr' => "Français",
         'it' => "Italien",
     ],
 
     'login' => [
         '2fa' => "Code du 2ème facteur",
         '2fa_desc' => "Le code du 2ème facteur est facultatif pour les utilisateurs qui n'ont pas configuré l'authentification à deux facteurs.",
         'forgot_password' => "Mot de passe oublié?",
         'header' => "Veuillez vous connecter",
         'sign_in' => "Se connecter",
         'webmail' => "Webmail"
     ],
 
     'meet' => [
         'title' => "Voix et vidéo-conférence",
         'welcome' => "Bienvenue dans notre programme bêta pour les conférences vocales et vidéo.",
         'url' => "Vous disposez d'une salle avec l'URL ci-dessous. Cette salle ouvre uniquement quand vous y êtes vous-même. Utilisez cette URL pour inviter des personnes à vous rejoindre.",
         'notice' => "Il s'agit d'un travail en évolution et d'autres fonctions seront ajoutées au fil du temps. Les fonctions actuelles sont les suivantes:",
         'sharing' => "Partage d'écran",
         'sharing-text' => "Partagez votre écran pour des présentations ou des exposés.",
         'security' => "sécurité de chambre",
         'security-text' => "Renforcez la sécurité de la salle en définissant un mot de passe que les participants devront connaître."
             . " avant de pouvoir entrer, ou verrouiller la porte afin que les participants doivent frapper, et un modérateur peut accepter ou refuser ces demandes.",
         'qa-title' => "Lever la main (Q&A)",
         'qa-text' => "Les membres du public silencieux peuvent lever la main pour animer une séance de questions-réponses avec les membres du panel.",
         'moderation' => "Délégation des Modérateurs",
         'moderation-text' => "Déléguer l'autorité du modérateur pour la séance, afin qu'un orateur ne soit pas inutilement"
             . " interrompu par l'arrivée des participants et d'autres tâches du modérateur.",
         'eject' => "Éjecter les participants",
         'eject-text' => "Éjectez les participants de la session afin de les obliger à se reconnecter ou de remédier aux violations des règles."
             . " Cliquez sur l'icône de l'utilisateur pour un renvoi effectif.",
         'silent' => "Membres du Public en Silence",
         'silent-text' => "Pour une séance de type webinaire, configurez la salle pour obliger tous les nouveaux participants à être des spectateurs silencieux.",
         'interpreters' => "Canaux d'Audio Spécifiques de Langues",
         'interpreters-text' => "Désignez un participant pour interpréter l'audio original dans une langue cible, pour les sessions avec des participants multilingues."
             . " L'interprète doit être capable de relayer l'audio original et de le remplacer.",
         'beta-notice' => "Rappelez-vous qu'il s'agit d'une version bêta et pourrait entraîner des problèmes."
             . " Au cas où vous rencontreriez des problèmes, n'hésitez pas à nous en faire part en contactant le support.",
 
         // Room options dialog
         'options' => "Options de salle",
         'password' => "Mot de passe",
         'password-none' => "aucun",
         'password-clear' => "Effacer mot de passe",
         'password-set' => "Définir le mot de passe",
         'password-text' => "Vous pouvez ajouter un mot de passe à votre session. Les participants devront fournir le mot de passe avant d'être autorisés à rejoindre la session.",
         'lock' => "Salle verrouillée",
         'lock-text' => "Lorsque la salle est verrouillée, les participants doivent être approuvés par un modérateur avant de pouvoir rejoindre la réunion.",
         'nomedia' => "Réservé aux abonnés",
         'nomedia-text' => "Force tous les participants à se joindre en tant qu'abonnés (avec caméra et microphone désactivés)"
             . "Les modérateurs pourront les promouvoir en tant qu'éditeurs tout au long de la session.",
 
         // Room menu
         'partcnt' => "Nombres de participants",
         'menu-audio-mute' => "Désactiver le son",
         'menu-audio-unmute' => "Activer le son",
         'menu-video-mute' => "Désactiver la vidéo",
         'menu-video-unmute' => "Activer la vidéo",
         'menu-screen' => "Partager l'écran",
         'menu-hand-lower' => "Baisser la main",
         'menu-hand-raise' => "Lever la main",
         'menu-channel' => "Canal de langue interprétée",
         'menu-chat' => "Le Chat",
         'menu-fullscreen' => "Plein écran",
         'menu-fullscreen-exit' => "Sortir en plein écran",
         'menu-leave' => "Quitter la session",
 
         // Room setup screen
         'setup-title' => "Préparez votre session",
         'mic' => "Microphone",
         'cam' => "Caméra",
         'nick' => "Surnom",
         'nick-placeholder' => "Votre nom",
         'join' => "JOINDRE",
         'joinnow' => "JOINDRE MAINTENANT",
         'imaowner' => "Je suis le propriétaire",
 
         // Room
         'qa' => "Q & A",
         'leave-title' => "Salle fermée",
         'leave-body' => "La session a été fermée par le propriétaire de la salle.",
         'media-title' => "Configuration des médias",
         'join-request' => "Demande de rejoindre",
         'join-requested' => "{user} demandé à rejoindre.",
 
         // Status messages
         'status-init' => "Vérification de la salle...",
         'status-323' => "La salle est fermée. Veuillez attendre le démarrage de la session par le propriétaire.",
         'status-324' => "La salle est fermée. Elle sera ouverte aux autres participants après votre adhésion.",
         'status-325' => "La salle est prête. Veuillez entrer un mot de passe valide.",
         'status-326' => "La salle est fermée. Veuillez entrer votre nom et réessayer.",
         'status-327' => "En attendant la permission de joindre la salle.",
         'status-404' => "La salle n'existe pas.",
         'status-429' => "Trop de demande. Veuillez, patienter.",
         'status-500' => "La connexion à la salle a échoué. Erreur de serveur.",
 
         // Other menus
         'media-setup' => "configuration des médias",
         'perm' => "Permissions",
         'perm-av' => "Publication d'audio et vidéo",
         'perm-mod' => "Modération",
         'lang-int' => "Interprète de langue",
         'menu-options' => "Options",
     ],
 
     'menu' => [
         'cockpit' => "Cockpit",
         'login' => "Connecter",
         'logout' => "Deconnecter",
         'signup' => "S'inscrire",
         'toggle' => "Basculer la navigation",
     ],
 
     'msg' => [
         'initializing' => "Initialisation...",
         'loading' => "Chargement...",
         'loading-failed' => "Échec du chargement des données.",
         'notfound' => "Resource introuvable.",
         'info' => "Information",
         'error' => "Erreur",
         'warning' => "Avertissement",
         'success' => "Succès",
     ],
 
     'nav' => [
         'more' => "Charger plus",
         'step' => "Étape {i}/{n}",
     ],
 
     'password' => [
         'reset' => "Réinitialiser le mot de passe",
         'reset-step1' => "Entrez votre adresse e-mail pour réinitialiser votre mot de passe.",
         'reset-step1-hint' => "Veuillez vérifier votre dossier de spam ou débloquer {email}.",
         'reset-step2' => "Nous avons envoyé un code de confirmation à votre adresse e-mail externe."
             . " Entrez le code que nous vous avons envoyé, ou cliquez sur le lien dans le message.",
     ],
 
     'resource' => [
         'create' => "Créer une ressource",
         'delete' => "Supprimer une ressource",
         'invitation-policy' => "Procédure d'invitation",
         'invitation-policy-text' => "Les invitations à des événements pour une ressource sont généralement acceptées automatiquement"
             . " si aucun événement n'est en conflit avec le temps demandé. La procédure d'invitation le permet"
             . " de rejeter ces demandes ou d'exiger une acceptation manuelle d'un utilisateur spécifique.",
         'ipolicy-manual' => "Manuel (provisoire)",
         'ipolicy-accept' => "Accepter",
         'ipolicy-reject' => "Rejecter",
         'list-title' => "Ressource | Ressources",
         'list-empty' => "Il y a aucune ressource sur ce compte.",
         'new' => "Nouvelle ressource",
     ],
 
     'shf' => [
         'create' => "Créer un dossier",
         'delete' => "Supprimer un dossier",
         'acl-text' => "Permet de définir les droits d'accès des utilisateurs au dossier partagé..",
         'list-title' => "Dossier partagé | Dossiers partagés",
         'list-empty' => "Il y a aucun dossier partagé dans ce compte.",
         'new' => "Nouvelle dossier",
         'type-mail' => "Courriel",
         'type-event' => "Calendrier",
         'type-contact' => "Carnet d'Adresses",
         'type-task' => "Tâches",
         'type-note' => "Notes",
         'type-file' => "Fichiers",
     ],
 
     'signup' => [
         'email' => "Adresse e-mail actuelle",
         'login' => "connecter",
         'title' => "S'inscrire",
         'step1' => "Inscrivez-vous pour commencer votre mois gratuit.",
         'step2' => "Nous avons envoyé un code de confirmation à votre adresse e-mail. Entrez le code que nous vous avons envoyé, ou cliquez sur le lien dans le message.",
         'step3' => "Créez votre identité Kolab (vous pourrez choisir des adresses supplémentaires plus tard).",
         'voucher' => "Coupon Code",
     ],
 
     'status' => [
         'prepare-account' => "Votre compte est en cours de préparation.",
         'prepare-domain' => "Le domain est en cours de préparation.",
         'prepare-distlist' => "La liste de distribution est en cours de préparation.",
         'prepare-shared-folder' => "Le dossier portagé est en cours de préparation.",
         'prepare-user' => "Le compte d'utilisateur est en cours de préparation.",
         'prepare-hint' => "Certaines fonctionnalités peuvent être manquantes ou en lecture seule pour le moment.",
         'prepare-refresh' => "Le processus ne se termine jamais? Appuyez sur le bouton \"Refresh\", s'il vous plaît.",
         'prepare-resource' => "Nous préparons la ressource.",
         'ready-account' => "Votre compte est presque prêt.",
         'ready-domain' => "Le domaine est presque prêt.",
         'ready-distlist' => "La liste de distribution est presque prête.",
         'ready-resource' => "La ressource est presque prête.",
         'ready-shared-folder' => "Le dossier partagé est presque prêt.",
         'ready-user' => "Le compte d'utilisateur est presque prêt.",
         'verify' => "Veuillez vérifier votre domaine pour terminer le processus de configuration.",
         'verify-domain' => "Vérifier domaine",
         'degraded' => "Dégradé",
         'deleted' => "Supprimé",
         'suspended' => "Suspendu",
         'notready' => "Pas Prêt",
         'active' => "Actif",
     ],
 
     'support' => [
         'title' => "Contacter Support",
         'id' => "Numéro de client ou adresse é-mail que vous avez chez nous.",
         'id-pl' => "e.g. 12345678 ou john@kolab.org",
         'id-hint' => "Laissez vide si vous n'êtes pas encore client",
         'name' => "Nom",
         'name-pl' => "comment nous devons vous adresser dans notre réponse",
         'email' => "adresse e-mail qui fonctionne",
         'email-pl' => "assurez-vous que nous pouvons vous atteindre à cette adresse",
         'summary' => "Résumé du problème",
         'summary-pl' => "une phrase qui résume votre situation",
         'expl' => "Analyse du problème",
     ],
 
     'user' => [
         '2fa-hint1' => "Cela éliminera le droit à l'authentification à 2-Facteurs ainsi que les éléments configurés par l'utilisateur.",
         '2fa-hint2' => "Veuillez vous assurer que l'identité de l'utilisateur est correctement confirmée.",
         'add-beta' => "Activer le programme bêta",
         'address' => "Adresse",
         'aliases' => "Alias",
-        'aliases-email' => "Alias E-mail",
         'aliases-none' => "Cet utilisateur n'aucune alias e-mail.",
         'add-bonus' => "Ajouter un bonus",
         'add-bonus-title' => "Ajouter un bonus au portefeuille",
         'add-penalty' => "Ajouter une pénalité",
         'add-penalty-title' => "Ajouter une pénalité au portefeuille",
         'auto-payment' => "Auto-paiement",
         'auto-payment-text' => "Recharger par <b>{amount}</b> quand le montant est inférieur à <b>{balance}</b> utilisant {method}",
         'country' => "Pays",
         'create' => "Créer un utilisateur",
         'custno' => "No. de Client.",
         'degraded-warning' => "Le compte est dégradé. Certaines fonctionnalités ont été désactivées.",
         'degraded-hint' => "Veuillez effectuer un paiement.",
         'delete' => "Supprimer Utilisateur",
         'delete-email' => "Supprimer {email}",
         'delete-text' => "Voulez-vous vraiment supprimer cet utilisateur de façon permanente?"
             . " Cela supprimera toutes les données du compte et retirera la permission d'accéder au compte d'e-email."
             . " Veuillez noter que cette action ne peut pas être révoquée.",
         'discount' => "Rabais",
         'discount-hint' => "rabais appliqué",
         'discount-title' => "Rabais de compte",
         'distlists' => "Listes de Distribution",
         'domains' => "Domaines",
+        'email-aliases' => "Alias E-mail",
         'ext-email' => "E-mail externe",
         'finances' => "Finances",
         'greylisting' => "Greylisting",
         'greylisting-text' => "La greylisting est une méthode de défense des utilisateurs contre le spam."
             . " Tout e-mail entrant provenant d'un expéditeur non reconnu est temporairement rejeté."
             . " Le serveur d'origine doit réessayer après un délai cette fois-ci, le mail sera accepté."
             . " Les spammeurs ne réessayent généralement pas de remettre le mail.",
         'list-title' => "Comptes d'utilisateur",
         'list-empty' => "Il n'y a aucun utilisateur dans ce compte.",
         'managed-by' => "Géré par",
         'new' => "Nouveau compte d'utilisateur",
         'org' => "Organisation",
         'package' => "Paquet",
         'price' => "Prix",
         'profile-title' => "Votre profile",
         'profile-delete' => "Supprimer compte",
         'profile-delete-title' => "Supprimer ce compte?",
         'profile-delete-text1' => "Cela supprimera le compte ainsi que tous les domaines, utilisateurs et alias associés à ce compte.",
         'profile-delete-warning' => "Cette opération est irrévocable",
         'profile-delete-text2' => "Comme vous ne pourrez plus rien récupérer après ce point, assurez-vous d'avoir migré toutes les données avant de poursuivre.",
         'profile-delete-support' => "Étant donné que nous nous attachons à toujours nous améliorer, nous aimerions vous demander 2 minutes de votre temps. "
             . "Le meilleur moyen de nous améliorer est le feedback des utilisateurs, et nous voudrions vous demander"
             . "quelques mots sur les raisons pour lesquelles vous avez quitté notre service. Veuillez envoyer vos commentaires au <a href=\"{href}\">{email}</a>.",
         'profile-delete-contact' => "Par ailleurs, n'hésitez pas à contacter le support de {app} pour toute question ou souci que vous pourriez avoir dans ce contexte.",
         'reset-2fa' => "Réinitialiser l'authentification à 2-Facteurs.",
         'reset-2fa-title' => "Réinitialisation de l'Authentification à 2-Facteurs",
         'resources' => "Ressources",
         'title' => "Compte d'utilisateur",
         'search' => "Adresse e-mail ou nom de l'utilisateur",
         'search-pl' => "ID utilisateur, e-mail ou domamine",
         'skureq' => "{sku} demande {list}.",
         'subscription' => "Subscription",
         'subscriptions' => "Subscriptions",
         'subscriptions-none' => "Cet utilisateur n'a pas de subscriptions.",
         'users' => "Utilisateurs",
     ],
 
     'wallet' => [
         'add-credit' => "Ajouter un crédit",
         'auto-payment-cancel' => "Annuler l'auto-paiement",
         'auto-payment-change' => "Changer l'auto-paiement",
         'auto-payment-failed' => "La configuration des paiements automatiques a échoué. Redémarrer le processus pour activer les top-ups automatiques.",
         'auto-payment-hint' => "Cela fonctionne de la manière suivante: Chaque fois que votre compte est épuisé, nous débiterons votre méthode de paiement préférée d'un montant que vous aurez défini."
             . " Vous pouvez annuler ou modifier l'option de paiement automatique à tout moment.",
         'auto-payment-setup' => "configurer l'auto-paiement",
         'auto-payment-disabled' => "L'auto-paiement configuré a été désactivé. Rechargez votre porte-monnaie ou augmentez le montant d'auto-paiement.",
         'auto-payment-info' => "L'auto-paiement est <b>set</b> pour recharger votre compte par <b>{amount}</b> lorsque le solde de votre compte devient inférieur à <b>{balance}</b>.",
         'auto-payment-inprogress' => "La configuration d'auto-paiement est toujours en cours.",
         'auto-payment-next' => "Ensuite, vous serez redirigé vers la page de paiement, où vous pourrez fournir les coordonnées de votre carte de crédit.",
         'auto-payment-disabled-next' => "L'auto-paiement est désactivé. Dès que vous aurez soumis de nouveaux paramètres, nous l'activerons et essaierons de recharger votre portefeuille.",
         'auto-payment-update' => "Mise à jour de l'auto-paiement.",
         'banktransfer-hint' => "Veuillez noter qu'un virement bancaire peut nécessiter plusieurs jours avant d'être effectué.",
         'currency-conv' => "Le principe est le suivant: Vous spécifiez le montant dont vous voulez recharger votre portefeuille en {wc}."
             . " Nous convertirons ensuite ce montant en {pc}, et sur la page suivante, vous obtiendrez les coordonnées bancaires pour transférer le montant en {pc}.",
         'fill-up' => "Recharger par",
         'history' => "Histoire",
         'month' => "mois",
         'noperm' => "Seuls les propriétaires de compte peuvent accéder à un portefeuille.",
         'payment-amount-hint' => "Choisissez le montant dont vous voulez recharger votre portefeuille.",
         'payment-method' => "Mode de paiement: {method}",
         'payment-warning' => "Vous serez facturé pour {price}.",
         'pending-payments' => "Paiements en attente",
         'pending-payments-warning' => "Vous avez des paiements qui sont encore en cours. Voir l'onglet \"Paiements en attente\" ci-dessous.",
         'pending-payments-none' => "Il y a aucun paiement en attente pour ce compte.",
         'receipts' => "Reçus",
         'receipts-hint' => "Vous pouvez télécharger ici les reçus (au format PDF) pour les paiements de la période spécifiée. Sélectionnez la période et appuyez sur le bouton Télécharger.",
         'receipts-none' => "Il y a aucun reçu pour les paiements de ce compte. Veuillez noter que vous pouvez télécharger les reçus après la fin du mois.",
         'title' => "Solde du compte",
         'top-up' => "Rechargez votre portefeuille",
         'transactions' => "Transactions",
         'transactions-none' => "Il y a aucun transaction pour ce compte.",
         'when-below' => "lorsque le solde du compte est inférieur à",
     ],
 ];
diff --git a/src/resources/vue/Admin/Distlist.vue b/src/resources/vue/Admin/Distlist.vue
index 76477f43..f7721947 100644
--- a/src/resources/vue/Admin/Distlist.vue
+++ b/src/resources/vue/Admin/Distlist.vue
@@ -1,113 +1,107 @@
 <template>
     <div v-if="list.id" class="container">
         <div class="card" id="distlist-info">
             <div class="card-body">
                 <div class="card-title">{{ list.email }}</div>
                 <div class="card-text">
                     <form class="read-only short">
                         <div class="row plaintext">
                             <label for="distlistid" class="col-sm-4 col-form-label">
                                 {{ $t('form.id') }} <span class="text-muted">({{ $t('form.created') }})</span>
                             </label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="distlistid">
                                     {{ list.id }} <span class="text-muted">({{ list.created_at }})</span>
                                 </span>
                             </div>
                         </div>
                         <div class="row plaintext">
                             <label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
                             <div class="col-sm-8">
                                 <span :class="$root.statusClass(list) + ' form-control-plaintext'" id="status">{{ $root.statusText(list) }}</span>
                             </div>
                         </div>
                         <div class="row plaintext">
                             <label for="name" class="col-sm-4 col-form-label">{{ $t('distlist.name') }}</label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="name">{{ list.name }}</span>
                             </div>
                         </div>
                         <div class="row plaintext">
                             <label for="members" class="col-sm-4 col-form-label">{{ $t('distlist.recipients') }}</label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="members">
                                     <span v-for="member in list.members" :key="member">{{ member }}<br></span>
                                 </span>
                             </div>
                         </div>
                     </form>
                     <div class="mt-2 buttons">
                         <button v-if="!list.isSuspended" id="button-suspend" class="btn btn-warning" type="button" @click="suspendList">
                             {{ $t('btn.suspend') }}
                         </button>
                         <button v-if="list.isSuspended" id="button-unsuspend" class="btn btn-warning" type="button" @click="unsuspendList">
                             {{ $t('btn.unsuspend') }}
                         </button>
                     </div>
                 </div>
             </div>
         </div>
-        <ul class="nav nav-tabs mt-3" role="tablist">
-            <li class="nav-item">
-                <a class="nav-link active" id="tab-settings" href="#distlist-settings" role="tab" aria-controls="distlist-settings" aria-selected="false" @click="$root.tab">
-                    {{ $t('form.settings') }}
-                </a>
-            </li>
-        </ul>
+        <tabs class="mt-3" :tabs="['form.settings']"></tabs>
         <div class="tab-content">
-            <div class="tab-pane show active" id="distlist-settings" role="tabpanel" aria-labelledby="tab-settings">
+            <div class="tab-pane show active" id="settings" role="tabpanel" aria-labelledby="tab-settings">
                 <div class="card-body">
                     <div class="card-text">
                         <form class="read-only short">
                             <div class="row plaintext">
                                 <label for="sender_policy" class="col-sm-4 col-form-label">{{ $t('distlist.sender-policy') }}</label>
                                 <div class="col-sm-8">
                                     <span class="form-control-plaintext" id="sender_policy">
                                         {{ list.config.sender_policy && list.config.sender_policy.length ? list.config.sender_policy.join(', ') : $t('form.none') }}
                                     </span>
                                 </div>
                             </div>
                         </form>
                     </div>
                 </div>
             </div>
         </div>
     </div>
 </template>
 
 <script>
     export default {
         data() {
             return {
                 list: { members: [], config: {} }
             }
         },
         created() {
             axios.get('/api/v4/groups/' + this.$route.params.list, { loader: true })
                 .then(response => {
                     this.list = response.data
                 })
                 .catch(this.$root.errorHandler)
         },
         methods: {
             suspendList() {
                 axios.post('/api/v4/groups/' + this.list.id + '/suspend')
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.list = Object.assign({}, this.list, { isSuspended: true })
                         }
                     })
             },
             unsuspendList() {
                 axios.post('/api/v4/groups/' + this.list.id + '/unsuspend')
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.list = Object.assign({}, this.list, { isSuspended: false })
                         }
                     })
             }
         }
     }
 </script>
diff --git a/src/resources/vue/Admin/Domain.vue b/src/resources/vue/Admin/Domain.vue
index 5bdf8803..5be2d9e7 100644
--- a/src/resources/vue/Admin/Domain.vue
+++ b/src/resources/vue/Admin/Domain.vue
@@ -1,118 +1,107 @@
 <template>
     <div v-if="domain" class="container">
         <div class="card" id="domain-info">
             <div class="card-body">
                 <div class="card-title">{{ domain.namespace }}</div>
                 <div class="card-text">
                     <form class="read-only short">
                         <div class="row plaintext">
                             <label for="domainid" class="col-sm-4 col-form-label">
                                 {{ $t('form.id') }} <span class="text-muted">({{ $t('form.created') }})</span>
                             </label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="domainid">
                                     {{ domain.id }} <span class="text-muted">({{ domain.created_at }})</span>
                                 </span>
                             </div>
                         </div>
                         <div class="row plaintext">
                             <label for="first_name" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="status">
                                     <span :class="$root.statusClass(domain)">{{ $root.statusText(domain) }}</span>
                                 </span>
                             </div>
                         </div>
                     </form>
                     <div class="mt-2 buttons">
                         <btn v-if="!domain.isSuspended" id="button-suspend" class="btn-warning" @click="suspendDomain">
                             {{ $t('btn.suspend') }}
                         </btn>
                         <btn v-if="domain.isSuspended" id="button-unsuspend" class="btn-warning" @click="unsuspendDomain">
                             {{ $t('btn.unsuspend') }}
                         </btn>
                     </div>
                 </div>
             </div>
         </div>
-        <ul class="nav nav-tabs mt-3" role="tablist">
-            <li class="nav-item">
-                <a class="nav-link active" id="tab-config" href="#domain-config" role="tab" aria-controls="domain-config" aria-selected="true" @click="$root.tab">
-                    {{ $t('form.config') }}
-                </a>
-            </li>
-            <li class="nav-item">
-                <a class="nav-link" id="tab-settings" href="#domain-settings" role="tab" aria-controls="domain-settings" aria-selected="false" @click="$root.tab">
-                    {{ $t('form.settings') }}
-                </a>
-            </li>
-        </ul>
+        <tabs class="mt-3" :tabs="['form.config', 'form.settings']"></tabs>
         <div class="tab-content">
-            <div class="tab-pane show active" id="domain-config" role="tabpanel" aria-labelledby="tab-config">
+            <div class="tab-pane show active" id="config" role="tabpanel" aria-labelledby="tab-config">
                 <div class="card-body">
                     <div class="card-text">
                         <p>{{ $t('domain.dns-verify') }}</p>
                         <p><pre id="dns-verify">{{ domain.dns.join("\n") }}</pre></p>
                         <p>{{ $t('domain.dns-config') }}</p>
                         <p><pre id="dns-config">{{ domain.mx.join("\n") }}</pre></p>
                     </div>
                 </div>
             </div>
-            <div class="tab-pane" id="domain-settings" role="tabpanel" aria-labelledby="tab-settings">
+            <div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
                 <div class="card-body">
                     <div class="card-text">
                         <form class="read-only short">
                             <div class="row plaintext">
                                 <label for="spf_whitelist" class="col-sm-4 col-form-label">{{ $t('domain.spf-whitelist') }}</label>
                                 <div class="col-sm-8">
                                     <span class="form-control-plaintext" id="spf_whitelist">
                                         {{ domain.config && domain.config.spf_whitelist.length ? domain.config.spf_whitelist.join(', ') : $t('form.none') }}
                                     </span>
                                 </div>
                             </div>
                         </form>
                     </div>
                 </div>
             </div>
         </div>
     </div>
 </template>
 
 <script>
     export default {
         data() {
             return {
                 domain: null
             }
         },
         created() {
             const domain_id = this.$route.params.domain;
 
             axios.get('/api/v4/domains/' + domain_id)
                 .then(response => {
                     this.domain = response.data
                 })
                 .catch(this.$root.errorHandler)
         },
         methods: {
             suspendDomain() {
                 axios.post('/api/v4/domains/' + this.domain.id + '/suspend')
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.domain = Object.assign({}, this.domain, { isSuspended: true })
                         }
                     })
             },
             unsuspendDomain() {
                 axios.post('/api/v4/domains/' + this.domain.id + '/unsuspend')
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.domain = Object.assign({}, this.domain, { isSuspended: false })
                         }
                     })
             }
         }
     }
 </script>
diff --git a/src/resources/vue/Admin/Resource.vue b/src/resources/vue/Admin/Resource.vue
index 7ddc0846..cef90e9f 100644
--- a/src/resources/vue/Admin/Resource.vue
+++ b/src/resources/vue/Admin/Resource.vue
@@ -1,77 +1,71 @@
 <template>
     <div v-if="resource.id" class="container">
         <div class="card" id="resource-info">
             <div class="card-body">
                 <div class="card-title">{{ resource.email }}</div>
                 <div class="card-text">
                     <form class="read-only short">
                         <div class="row plaintext">
                             <label for="resourceid" class="col-sm-4 col-form-label">
                                 {{ $t('form.id') }} <span class="text-muted">({{ $t('form.created') }})</span>
                             </label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="resourceid">
                                     {{ resource.id }} <span class="text-muted">({{ resource.created_at }})</span>
                                 </span>
                             </div>
                         </div>
                         <div class="row plaintext">
                             <label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
                             <div class="col-sm-8">
                                 <span :class="$root.statusClass(resource) + ' form-control-plaintext'" id="status">{{ $root.statusText(resource) }}</span>
                             </div>
                         </div>
                         <div class="row plaintext">
                             <label for="name" class="col-sm-4 col-form-label">{{ $t('form.name') }}</label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="name">{{ resource.name }}</span>
                             </div>
                         </div>
                     </form>
                 </div>
             </div>
         </div>
-        <ul class="nav nav-tabs mt-3" role="tablist">
-            <li class="nav-item">
-                <a class="nav-link active" id="tab-settings" href="#resource-settings" role="tab" aria-controls="resource-settings" aria-selected="false" @click="$root.tab">
-                    {{ $t('form.settings') }}
-                </a>
-            </li>
-        </ul>
+        <tabs class="mt-3" :tabs="['form.settings']"></tabs>
         <div class="tab-content">
-            <div class="tab-pane show active" id="resource-settings" role="tabpanel" aria-labelledby="tab-settings">
+            <div class="tab-pane show active" id="settings" role="tabpanel" aria-labelledby="tab-settings">
                 <div class="card-body">
                     <div class="card-text">
                         <form class="read-only short">
                             <div class="row plaintext">
                                 <label for="invitation_policy" class="col-sm-4 col-form-label">{{ $t('resource.invitation-policy') }}</label>
                                 <div class="col-sm-8">
                                     <span class="form-control-plaintext" id="invitation_policy">
                                         {{ resource.config.invitation_policy || $t('form.none') }}
                                     </span>
                                 </div>
                             </div>
                         </form>
                     </div>
                 </div>
             </div>
         </div>
     </div>
 </template>
 
 <script>
     export default {
         data() {
             return {
                 resource: { config: {} }
             }
         },
         created() {
             axios.get('/api/v4/resources/' + this.$route.params.resource, { loader: true })
                 .then(response => {
                     this.resource = response.data
                 })
                 .catch(this.$root.errorHandler)
         }
     }
 </script>
diff --git a/src/resources/vue/Admin/SharedFolder.vue b/src/resources/vue/Admin/SharedFolder.vue
index bb268157..38ced256 100644
--- a/src/resources/vue/Admin/SharedFolder.vue
+++ b/src/resources/vue/Admin/SharedFolder.vue
@@ -1,114 +1,108 @@
 <template>
     <div v-if="folder.id" class="container">
         <div class="card" id="folder-info">
             <div class="card-body">
                 <div class="card-title">{{ folder.email }}</div>
                 <div class="card-text">
                     <form class="read-only short">
                         <div class="row plaintext">
                             <label for="folderid" class="col-sm-4 col-form-label">
                                 {{ $t('form.id') }} <span class="text-muted">({{ $t('form.created') }})</span>
                             </label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="folderid">
                                     {{ folder.id }} <span class="text-muted">({{ folder.created_at }})</span>
                                 </span>
                             </div>
                         </div>
                         <div class="row plaintext">
                             <label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
                             <div class="col-sm-8">
                                 <span :class="$root.statusClass(folder) + ' form-control-plaintext'" id="status">{{ $root.statusText(folder) }}</span>
                             </div>
                         </div>
                         <div class="row plaintext">
                             <label for="name" class="col-sm-4 col-form-label">{{ $t('form.name') }}</label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="name">{{ folder.name }}</span>
                             </div>
                         </div>
                         <div class="row plaintext">
                             <label for="type" class="col-sm-4 col-form-label">{{ $t('form.type') }}</label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="type">{{ $t('shf.type-' + folder.type) }}</span>
                             </div>
                         </div>
                     </form>
                 </div>
             </div>
         </div>
-        <ul class="nav nav-tabs mt-3" role="tablist">
-            <li class="nav-item">
-                <a class="nav-link active" id="tab-settings" href="#folder-settings" role="tab" aria-controls="folder-settings" aria-selected="false" @click="$root.tab">
-                    {{ $t('form.settings') }}
-                </a>
-            </li>
-            <li class="nav-item">
-                <a class="nav-link" id="tab-aliases" href="#folder-aliases" role="tab" aria-controls="folder-aliases" aria-selected="false" @click="$root.tab">
-                    {{ $t('user.aliases-email') }} ({{ folder.aliases.length }})
-                </a>
-            </li>
-        </ul>
+        <tabs class="mt-3" :tabs="tabs" ref="tabs"></tabs>
         <div class="tab-content">
-            <div class="tab-pane show active" id="folder-settings" role="tabpanel" aria-labelledby="tab-settings">
+            <div class="tab-pane show active" id="settings" role="tabpanel" aria-labelledby="tab-settings">
                 <div class="card-body">
                     <div class="card-text">
                         <form class="read-only short">
                             <div class="row plaintext">
                                 <label for="acl" class="col-sm-4 col-form-label">{{ $t('form.acl') }}</label>
                                 <div class="col-sm-8">
                                     <span class="form-control-plaintext" id="acl">
                                         <span v-if="folder.config.acl.length">
                                             <span v-for="(entry, index) in folder.config.acl" :key="index">
                                                 {{ entry.replace(',', ':') }}<br>
                                             </span>
                                         </span>
                                         <span v-else>{{ $t('form.none') }}</span>
                                     </span>
                                 </div>
                             </div>
                         </form>
                     </div>
                 </div>
             </div>
-            <div class="tab-pane" id="folder-aliases" role="tabpanel" aria-labelledby="tab-aliases">
+            <div class="tab-pane" id="aliases" role="tabpanel" aria-labelledby="tab-aliases">
                 <div class="card-body">
                     <div class="card-text">
                         <list-table :list="folder.aliases" :setup="aliasesListSetup" class="mb-0"></list-table>
                     </div>
                 </div>
             </div>
         </div>
     </div>
 </template>
 
 <script>
     import { ListTable } from '../Widgets/ListTools'
 
     export default {
         components: {
             ListTable
         },
         data() {
             return {
                 aliasesListSetup: {
                     columns: [
                         {
                             prop: 'email',
                             content: item => item
                         },
                     ],
                     footLabel: 'shf.aliases-none'
                 },
-                folder: { config: {}, aliases: [] }
+                folder: { config: {}, aliases: [] },
+                tabs: [
+                    { label: 'form.settings' },
+                    { label: 'user.email-aliases', count: 0 }
+                ]
             }
         },
         created() {
             axios.get('/api/v4/shared-folders/' + this.$route.params.folder, { loader: true })
                 .then(response => {
                     this.folder = response.data
+                    this.tabs[1].count = this.folder.aliases.length
                 })
                 .catch(this.$root.errorHandler)
         }
     }
 </script>
diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue
index 75c160f1..09197126 100644
--- a/src/resources/vue/Admin/User.vue
+++ b/src/resources/vue/Admin/User.vue
@@ -1,644 +1,619 @@
 <template>
     <div class="container">
         <div class="card" id="user-info">
             <div class="card-body">
                 <h1 class="card-title">{{ user.email }}</h1>
                 <div class="card-text">
                     <form class="read-only short">
                         <div v-if="user.wallet.user_id != user.id" class="row plaintext">
                             <label for="manager" class="col-sm-4 col-form-label">{{ $t('user.managed-by') }}</label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="manager">
                                     <router-link :to="{ path: '/user/' + user.wallet.user_id }">{{ user.wallet.user_email }}</router-link>
                                 </span>
                             </div>
                         </div>
                         <div class="row plaintext">
                             <label for="userid" class="col-sm-4 col-form-label">ID <span class="text-muted">({{ $t('form.created') }})</span></label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="userid">
                                     {{ user.id }} <span class="text-muted">({{ user.created_at }})</span>
                                 </span>
                             </div>
                         </div>
                         <div class="row plaintext">
                             <label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="status">
                                     <span :class="$root.statusClass(user)">{{ $root.statusText(user) }}</span>
                                 </span>
                             </div>
                         </div>
                         <div class="row plaintext" v-if="user.first_name">
                             <label for="first_name" class="col-sm-4 col-form-label">{{ $t('form.firstname') }}</label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="first_name">{{ user.first_name }}</span>
                             </div>
                         </div>
                         <div class="row plaintext" v-if="user.last_name">
                             <label for="last_name" class="col-sm-4 col-form-label">{{ $t('form.lastname') }}</label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="last_name">{{ user.last_name }}</span>
                             </div>
                         </div>
                         <div class="row plaintext" v-if="user.organization">
                             <label for="organization" class="col-sm-4 col-form-label">{{ $t('user.org') }}</label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="organization">{{ user.organization }}</span>
                             </div>
                         </div>
                         <div class="row plaintext" v-if="user.phone">
                             <label for="phone" class="col-sm-4 col-form-label">{{ $t('form.phone') }}</label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="phone">{{ user.phone }}</span>
                             </div>
                         </div>
                         <div class="row plaintext">
                             <label for="external_email" class="col-sm-4 col-form-label">{{ $t('user.ext-email') }}</label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="external_email">
                                     <a v-if="user.external_email" :href="'mailto:' + user.external_email">{{ user.external_email }}</a>
                                     <btn class="btn-secondary btn-sm ms-2" @click="emailEdit">{{ $t('btn.edit') }}</btn>
                                 </span>
                             </div>
                         </div>
                         <div class="row plaintext" v-if="user.billing_address">
                             <label for="billing_address" class="col-sm-4 col-form-label">{{ $t('user.address') }}</label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" style="white-space:pre" id="billing_address">{{ user.billing_address }}</span>
                             </div>
                         </div>
                         <div class="row plaintext">
                             <label for="country" class="col-sm-4 col-form-label">{{ $t('user.country') }}</label>
                             <div class="col-sm-8">
                                 <span class="form-control-plaintext" id="country">{{ user.country }}</span>
                             </div>
                         </div>
                     </form>
                     <div class="mt-2 buttons">
                         <btn v-if="!user.isSuspended" id="button-suspend" class="btn-warning" @click="suspendUser">
                             {{ $t('btn.suspend') }}
                         </btn>
                         <btn v-if="user.isSuspended" id="button-unsuspend" class="btn-warning" @click="unsuspendUser">
                             {{ $t('btn.unsuspend') }}
                         </btn>
                     </div>
                 </div>
             </div>
         </div>
-        <ul class="nav nav-tabs mt-3" role="tablist">
-            <li class="nav-item">
-                <a class="nav-link active" id="tab-finances" href="#user-finances" role="tab" aria-controls="user-finances" aria-selected="true">
-                    {{ $t('user.finances') }}
-                </a>
-            </li>
-            <li class="nav-item">
-                <a class="nav-link" id="tab-aliases" href="#user-aliases" role="tab" aria-controls="user-aliases" aria-selected="false">
-                    {{ $t('user.aliases') }} ({{ user.aliases.length }})
-                </a>
-            </li>
-            <li class="nav-item">
-                <a class="nav-link" id="tab-subscriptions" href="#user-subscriptions" role="tab" aria-controls="user-subscriptions" aria-selected="false">
-                    {{ $t('user.subscriptions') }} ({{ skus.length }})
-                </a>
-            </li>
-            <li class="nav-item">
-                <a class="nav-link" id="tab-domains" href="#user-domains" role="tab" aria-controls="user-domains" aria-selected="false">
-                    {{ $t('user.domains') }} ({{ domains.length }})
-                </a>
-            </li>
-            <li class="nav-item">
-                <a class="nav-link" id="tab-users" href="#user-users" role="tab" aria-controls="user-users" aria-selected="false">
-                    {{ $t('user.users') }} ({{ users.length }})
-                </a>
-            </li>
-            <li class="nav-item">
-                <a class="nav-link" id="tab-distlists" href="#user-distlists" role="tab" aria-controls="user-distlists" aria-selected="false">
-                    {{ $t('user.distlists') }} ({{ distlists.length }})
-                </a>
-            </li>
-            <li class="nav-item">
-                <a class="nav-link" id="tab-resources" href="#user-resources" role="tab" aria-controls="user-resources" aria-selected="false">
-                    {{ $t('user.resources') }} ({{ resources.length }})
-                </a>
-            </li>
-            <li class="nav-item">
-                <a class="nav-link" id="tab-shared-folders" href="#user-shared-folders" role="tab" aria-controls="user-shared-folders" aria-selected="false">
-                    {{ $t('dashboard.shared-folders') }} ({{ folders.length }})
-                </a>
-            </li>
-            <li class="nav-item">
-                <a class="nav-link" id="tab-settings" href="#user-settings" role="tab" aria-controls="user-settings" aria-selected="false">
-                    {{ $t('form.settings') }}
-                </a>
-            </li>
-        </ul>
+        <tabs class="mt-3" :tabs="tabs" ref="tabs"></tabs>
         <div class="tab-content">
-            <div class="tab-pane show active" id="user-finances" role="tabpanel" aria-labelledby="tab-finances">
+            <div class="tab-pane show active" id="finances" role="tabpanel" aria-labelledby="tab-finances">
                 <div class="card-body">
                     <h2 class="card-title">
                         {{ $t('wallet.title') }}
                         <span :class="wallet.balance < 0 ? 'text-danger' : 'text-success'"><strong>{{ $root.price(wallet.balance, wallet.currency) }}</strong></span>
                     </h2>
                     <div class="card-text">
                         <form class="read-only short">
                             <div class="row">
                                 <label class="col-sm-4 col-form-label">{{ $t('user.discount') }}</label>
                                 <div class="col-sm-8">
                                     <span class="form-control-plaintext" id="discount">
                                         <span>{{ wallet.discount ? (wallet.discount + '% - ' + wallet.discount_description) : 'none' }}</span>
                                         <btn class="btn-secondary btn-sm ms-2" @click="discountEdit">{{ $t('btn.edit') }}</btn>
                                     </span>
                                 </div>
                             </div>
                             <div class="row" v-if="wallet.mandate && wallet.mandate.id">
                                 <label class="col-sm-4 col-form-label">{{ $t('user.auto-payment') }}</label>
                                 <div class="col-sm-8">
                                     <span id="autopayment" :class="'form-control-plaintext' + (wallet.mandateState ? ' text-danger' : '')"
                                           v-html="$t('user.auto-payment-text', {
                                               amount: wallet.mandate.amount + ' ' + wallet.currency,
                                               balance: wallet.mandate.balance + ' ' + wallet.currency,
                                               method: wallet.mandate.method
                                           })"
                                     >
                                         <span v-if="wallet.mandateState">({{ wallet.mandateState }})</span>.
                                     </span>
                                 </div>
                             </div>
                             <div class="row" v-if="wallet.providerLink">
                                 <label class="col-sm-4 col-form-label">{{ capitalize(wallet.provider) }} {{ $t('form.id') }}</label>
                                 <div class="col-sm-8">
                                     <span class="form-control-plaintext" v-html="wallet.providerLink"></span>
                                 </div>
                             </div>
                         </form>
                         <div class="mt-2 buttons">
                             <btn id="button-award" class="btn-success" @click="awardDialog">{{ $t('user.add-bonus') }}</btn>
                             <btn id="button-penalty" class="btn-danger" @click="penalizeDialog">{{ $t('user.add-penalty') }}</btn>
                         </div>
                     </div>
                     <h2 class="card-title mt-4">{{ $t('wallet.transactions') }}</h2>
                     <transaction-log v-if="wallet.id && !walletReload" class="card-text" :wallet-id="wallet.id" :is-admin="true"></transaction-log>
                 </div>
             </div>
-            <div class="tab-pane" id="user-aliases" role="tabpanel" aria-labelledby="tab-aliases">
+            <div class="tab-pane" id="aliases" role="tabpanel" aria-labelledby="tab-aliases">
                 <div class="card-body">
                     <div class="card-text">
                         <list-table :list="user.aliases" :setup="aliasesListSetup" class="mb-0"></list-table>
                     </div>
                 </div>
             </div>
-            <div class="tab-pane" id="user-subscriptions" role="tabpanel" aria-labelledby="tab-subscriptions">
+            <div class="tab-pane" id="subscriptions" role="tabpanel" aria-labelledby="tab-subscriptions">
                 <div class="card-body">
                     <div class="card-text">
                         <list-table :list="skus" :setup="skusListSetup" class="mb-0"></list-table>
                         <small v-if="discount > 0" class="hint">
                             <hr class="m-0">
                             &sup1; {{ $t('user.discount-hint') }}: {{ discount }}% - {{ discount_description }}
                         </small>
                         <div class="mt-2 buttons">
                             <btn class="btn-danger" id="reset2fa" v-if="has2FA" @click="$refs.reset2faDialog.show()">{{ $t('user.reset-2fa') }}</btn>
                             <btn class="btn-secondary" id="addbetasku" v-if="!hasBeta" @click="addBetaSku">{{ $t('user.add-beta') }}</btn>
                         </div>
                     </div>
                 </div>
             </div>
-            <div class="tab-pane" id="user-domains" role="tabpanel" aria-labelledby="tab-domains">
+            <div class="tab-pane" id="domains" role="tabpanel" aria-labelledby="tab-domains">
                 <div class="card-body">
                     <div class="card-text">
                         <domain-list :list="domains" class="mb-0"></domain-list>
                     </div>
                 </div>
             </div>
-            <div class="tab-pane" id="user-users" role="tabpanel" aria-labelledby="tab-users">
+            <div class="tab-pane" id="users" role="tabpanel" aria-labelledby="tab-users">
                 <div class="card-body">
                     <div class="card-text">
                         <user-list :list="users" :current="user" class="mb-0"></user-list>
                     </div>
                 </div>
             </div>
-            <div class="tab-pane" id="user-distlists" role="tabpanel" aria-labelledby="tab-distlists">
+            <div class="tab-pane" id="distlists" role="tabpanel" aria-labelledby="tab-distlists">
                 <div class="card-body">
                     <div class="card-text">
                         <distlist-list :list="distlists" class="mb-0"></distlist-list>
                     </div>
                 </div>
             </div>
-            <div class="tab-pane" id="user-resources" role="tabpanel" aria-labelledby="tab-resources">
+            <div class="tab-pane" id="resources" role="tabpanel" aria-labelledby="tab-resources">
                 <div class="card-body">
                     <div class="card-text">
                         <resource-list :list="resources" class="mb-0"></resource-list>
                     </div>
                 </div>
             </div>
-            <div class="tab-pane" id="user-shared-folders" role="tabpanel" aria-labelledby="tab-shared-folders">
+            <div class="tab-pane" id="folders" role="tabpanel" aria-labelledby="tab-folders">
                 <div class="card-body">
                     <div class="card-text">
                         <shared-folder-list :list="folders" :with-email="true" class="mb-0"></shared-folder-list>
                     </div>
                 </div>
             </div>
-            <div class="tab-pane" id="user-settings" role="tabpanel" aria-labelledby="tab-settings">
+            <div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
                 <div class="card-body">
                     <div class="card-text">
                         <form class="read-only short">
                             <div class="row plaintext">
                                 <label for="greylist_enabled" class="col-sm-4 col-form-label">{{ $t('user.greylisting') }}</label>
                                 <div class="col-sm-8">
                                     <span class="form-control-plaintext" id="greylist_enabled">
                                         <span v-if="user.config.greylist_enabled" class="text-success">{{ $t('form.enabled') }}</span>
                                         <span v-else class="text-danger">{{ $t('form.disabled') }}</span>
                                     </span>
                                 </div>
                             </div>
                         </form>
                     </div>
                 </div>
             </div>
         </div>
 
         <modal-dialog id="discount-dialog" ref="discountDialog" :title="$t('user.discount-title')" @click="submitDiscount()" :buttons="['submit']">
             <div>
                 <select v-model="wallet.discount_id" class="form-select">
                     <option value="">- {{ $t('form.none') }} -</option>
                     <option v-for="item in discounts" :value="item.id" :key="item.id">{{ item.label }}</option>
                 </select>
             </div>
         </modal-dialog>
 
         <modal-dialog id="email-dialog" ref="emailDialog" :title="$t('user.ext-email')" @click="submitEmail()" :buttons="['submit']">
             <div>
                 <input v-model="external_email" name="external_email" class="form-control">
             </div>
         </modal-dialog>
 
         <modal-dialog id="oneoff-dialog" ref="oneoffDialog" @click="submitOneOff()" :buttons="['submit']"
                       :title="$t(oneoff_negative ? 'user.add-penalty-title' : 'user.add-bonus-title')"
         >
             <form data-validation-prefix="oneoff_">
                 <div class="mb-3">
                     <label for="oneoff_amount" class="form-label">{{ $t('form.amount') }}</label>
                     <div class="input-group">
                         <input type="text" class="form-control" id="oneoff_amount" v-model="oneoff_amount" required>
                         <span class="input-group-text">{{ wallet.currency }}</span>
                     </div>
                 </div>
                 <div>
                     <label for="oneoff_description" class="form-label">{{ $t('form.description') }}</label>
                     <input class="form-control" id="oneoff_description" v-model="oneoff_description" required>
                 </div>
             </form>
         </modal-dialog>
 
         <modal-dialog id="reset-2fa-dialog" ref="reset2faDialog" :title="$t('user.reset-2fa-title')" @click="reset2FA()"
                       :buttons="[{className: 'btn-danger modal-action', label: 'btn.reset'}]"
         >
             <p>{{ $t('user.2fa-hint1') }}</p>
             <p>{{ $t('user.2fa-hint2') }}</p>
         </modal-dialog>
     </div>
 </template>
 
 <script>
     import ModalDialog from '../Widgets/ModalDialog'
     import TransactionLog from '../Widgets/TransactionLog'
     import { ListTable } from '../Widgets/ListTools'
     import { default as DistlistList } from '../Distlist/ListWidget'
     import { default as DomainList } from '../Domain/ListWidget'
     import { default as ResourceList } from '../Resource/ListWidget'
     import { default as SharedFolderList } from '../SharedFolder/ListWidget'
     import { default as UserList } from '../User/ListWidget'
 
     import { library } from '@fortawesome/fontawesome-svg-core'
 
     library.add(
         require('@fortawesome/free-solid-svg-icons/faFolderOpen').definition,
         require('@fortawesome/free-solid-svg-icons/faGear').definition,
         require('@fortawesome/free-solid-svg-icons/faGlobe').definition,
         require('@fortawesome/free-solid-svg-icons/faUsers').definition,
     )
 
     export default {
         components: {
             DistlistList,
             DomainList,
             ListTable,
             ModalDialog,
             ResourceList,
             SharedFolderList,
             TransactionLog,
             UserList
         },
         beforeRouteUpdate (to, from, next) {
             // An event called when the route that renders this component has changed,
             // but this component is reused in the new route.
             // Required to handle links from /user/XXX to /user/YYY
             next()
             this.$parent.routerReload()
         },
         data() {
             return {
                 aliasesListSetup: {
                     columns: [
                         {
                             prop: 'email',
                             content: item => item
                         },
                     ],
                     footLabel: 'user.aliases-none'
                 },
                 oneoff_amount: '',
                 oneoff_description: '',
                 oneoff_negative: false,
                 discount: 0,
                 discount_description: '',
                 discounts: [],
                 external_email: '',
                 folders: [],
                 has2FA: false,
                 hasBeta: false,
                 wallet: {},
                 walletReload: false,
                 distlists: [],
                 domains: [],
                 resources: [],
                 sku2FA: null,
                 skus: [],
                 skusListSetup: {
                     columns: [
                         {
                             prop: 'name',
                             label: 'user.subscription'
                         },
                         {
                             prop: 'price',
                             className: 'price',
                             label: 'user.price'
                         }
                     ],
                     footLabel: 'user.subscriptions-none',
                     model: 'sku'
                 },
+                tabs: [
+                    { label: 'user.finances' },
+                    { label: 'user.aliases', count: 0 },
+                    { label: 'user.subscriptions', count: 0 },
+                    { label: 'user.domains', count: 0 },
+                    { label: 'user.users', count: 0 },
+                    { label: 'user.distlists', count: 0 },
+                    { label: 'user.resources', count: 0 },
+                    { label: 'dashboard.shared-folders', count: 0 },
+                    { label: 'form.settings' }
+                ],
                 users: [],
                 user: {
                     aliases: [],
                     config: {},
                     wallet: {},
                     skus: {},
                 }
             }
         },
         created() {
             const user_id = this.$route.params.user
 
             axios.get('/api/v4/users/' + user_id, { loader: true })
                 .then(response => {
                     this.user = response.data
 
-                    const loader = '#user-finances'
+                    const loader = '#finances'
                     const keys = ['first_name', 'last_name', 'external_email', 'billing_address', 'phone', 'organization']
 
                     let country = this.user.settings.country
                     if (country && country in window.config.countries) {
                         country = window.config.countries[country][1]
                     }
 
                     this.user.country = country
 
                     keys.forEach(key => { this.user[key] = this.user.settings[key] })
 
                     this.discount = this.user.wallet.discount
                     this.discount_description = this.user.wallet.discount_description
 
+                    this.$refs.tabs.updateCounter('aliases', this.user.aliases.length)
+
                     // TODO: currencies, multi-wallets, accounts
                     // Get more info about the wallet (e.g. payment provider related)
                     axios.get('/api/v4/wallets/' + this.user.wallets[0].id, { loader })
                         .then(response => {
                             this.wallet = response.data
                             this.setMandateState()
                         })
 
                     // Create subscriptions list
                     axios.get('/api/v4/users/' + user_id + '/skus')
                         .then(response => {
                             // "merge" SKUs with user entitlement-SKUs
                             response.data.forEach(sku => {
                                 const userSku = this.user.skus[sku.id]
                                 if (userSku) {
                                     let cost = userSku.costs.reduce((sum, current) => sum + current)
                                     let item = {
                                         id: sku.id,
                                         name: sku.name,
                                         cost: cost,
                                         price: this.$root.priceLabel(cost, this.discount)
                                     }
 
                                     if (sku.range) {
                                         item.name += ' ' + userSku.count + ' ' + sku.range.unit
                                     }
 
                                     this.skus.push(item)
 
                                     if (sku.handler == 'Auth2F') {
                                         this.has2FA = true
                                         this.sku2FA = sku.id
                                     } else if (sku.handler == 'Beta') {
                                         this.hasBeta = true
                                     }
                                 }
                             })
+
+                            this.$refs.tabs.updateCounter('subscriptions', this.skus.length)
                         })
 
                     // Fetch users
                     // TODO: Multiple wallets
                     axios.get('/api/v4/users?owner=' + user_id)
                         .then(response => {
                             this.users = response.data.list;
+                            this.$refs.tabs.updateCounter('users', this.users.length)
                         })
 
                     // Fetch domains
                     axios.get('/api/v4/domains?owner=' + user_id)
                         .then(response => {
                             this.domains = response.data.list
+                            this.$refs.tabs.updateCounter('domains', this.domains.length)
                         })
 
                     // Fetch distribution lists
                     axios.get('/api/v4/groups?owner=' + user_id)
                         .then(response => {
                             this.distlists = response.data.list
+                            this.$refs.tabs.updateCounter('distlists', this.distlists.length)
                         })
 
                     // Fetch resources lists
                     axios.get('/api/v4/resources?owner=' + user_id)
                         .then(response => {
                             this.resources = response.data.list
+                            this.$refs.tabs.updateCounter('resources', this.resources.length)
                         })
 
                     // Fetch shared folders lists
                     axios.get('/api/v4/shared-folders?owner=' + user_id)
                         .then(response => {
                             this.folders = response.data.list
+                            this.$refs.tabs.updateCounter('folders', this.folders.length)
                         })
                 })
                 .catch(this.$root.errorHandler)
         },
         mounted() {
-            $(this.$el).find('ul.nav-tabs a').on('click', this.$root.tab)
-
             this.$refs.discountDialog.events({
                 shown: () => {
                     // Note: Vue v-model is strict, convert null to a string
                     this.wallet.discount_id = this.wallet.discount_id || ''
                 }
             })
         },
         methods: {
             addBetaSku() {
                 axios.post('/api/v4/users/' + this.user.id + '/skus/beta')
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.hasBeta = true
                             const sku = response.data.sku
                             this.skus.push({
                                 id: sku.id,
                                 name: sku.name,
                                 cost: sku.cost,
                                 price: this.$root.priceLabel(sku.cost, this.discount)
                             })
+
+                            this.$refs.tabs.updateCounter('subscriptions', this.skus.length)
                         }
                     })
             },
             capitalize(str) {
                 return str.charAt(0).toUpperCase() + str.slice(1)
             },
             awardDialog() {
                 this.oneOffDialog(false)
             },
             discountEdit() {
                 this.$refs.discountDialog.show()
 
                 if (!this.discounts.length) {
                     // Fetch discounts
                     axios.get('/api/v4/users/' + this.user.id + '/discounts')
                         .then(response => {
                             this.discounts = response.data.list
                         })
                 }
             },
             emailEdit() {
                 this.external_email = this.user.external_email
                 this.$root.clearFormValidation($('#email-dialog'))
                 this.$refs.emailDialog.show()
             },
             setMandateState() {
                 let mandate = this.wallet.mandate
                 if (mandate && mandate.id) {
                     if (!mandate.isValid) {
                         this.wallet.mandateState = mandate.isPending ? 'pending' : 'invalid'
                     } else if (mandate.isDisabled) {
                         this.wallet.mandateState = 'disabled'
                     }
                 }
             },
             oneOffDialog(negative) {
                 this.oneoff_negative = negative
                 this.$refs.oneoffDialog.show()
             },
             penalizeDialog() {
                 this.oneOffDialog(true)
             },
             reload() {
                 // this is to reload transaction log
                 this.walletReload = true
                 this.$nextTick(() => { this.walletReload = false })
             },
             reset2FA() {
                 this.$refs.reset2faDialog.hide()
                 axios.post('/api/v4/users/' + this.user.id + '/reset2FA')
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.skus = this.skus.filter(sku => sku.id != this.sku2FA)
                             this.has2FA = false
+                            this.$refs.tabs.updateCounter('subscriptions', this.skus.length)
                         }
                     })
             },
             submitDiscount() {
                 this.$refs.discountDialog.hide()
 
                 axios.put('/api/v4/wallets/' + this.user.wallets[0].id, { discount: this.wallet.discount_id })
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.wallet = Object.assign({}, this.wallet, response.data)
 
                             // Update prices in Subscriptions tab
                             if (this.user.wallet.id == response.data.id) {
                                 this.discount = this.wallet.discount
                                 this.discount_description = this.wallet.discount_description
 
                                 this.skus.forEach(sku => {
                                     sku.price = this.$root.priceLabel(sku.cost, this.discount)
                                 })
                             }
                         }
                     })
             },
             submitEmail() {
                 axios.put('/api/v4/users/' + this.user.id, { external_email: this.external_email })
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$refs.emailDialog.hide()
                             this.$toast.success(response.data.message)
                             this.user.external_email = this.external_email
                             this.external_email = null // required because of Vue
                         }
                     })
             },
             submitOneOff() {
                 let wallet_id = this.user.wallets[0].id
                 let post = {
                     amount: this.oneoff_amount,
                     description: this.oneoff_description
                 }
 
                 if (this.oneoff_negative && /^\d+(\.?\d+)?$/.test(post.amount)) {
                     post.amount *= -1
                 }
 
                 this.$root.clearFormValidation('#oneoff-dialog')
 
                 axios.post('/api/v4/wallets/' + wallet_id + '/one-off', post)
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$refs.oneoffDialog.hide()
                             this.$toast.success(response.data.message)
                             this.wallet = Object.assign({}, this.wallet, {balance: response.data.balance})
                             this.oneoff_amount = ''
                             this.oneoff_description = ''
                             this.reload()
                         }
                     })
             },
             suspendUser() {
                 axios.post('/api/v4/users/' + this.user.id + '/suspend')
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.user = Object.assign({}, this.user, { isSuspended: true })
                         }
                     })
             },
             unsuspendUser() {
                 axios.post('/api/v4/users/' + this.user.id + '/unsuspend')
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.user = Object.assign({}, this.user, { isSuspended: false })
                         }
                     })
             }
         }
     }
 </script>
diff --git a/src/resources/vue/CompanionApp.vue b/src/resources/vue/CompanionApp.vue
index 211692de..ac8dc514 100644
--- a/src/resources/vue/CompanionApp.vue
+++ b/src/resources/vue/CompanionApp.vue
@@ -1,73 +1,59 @@
 <template>
     <div class="container" dusk="companionapp-component">
         <div class="card">
             <div class="card-body">
                 <div class="card-title">
                     {{ $t('companion.title') }}
                     <small><sup class="badge bg-primary">{{ $t('dashboard.beta') }}</sup></small>
                 </div>
                 <div class="card-text">
                     <p>
                         {{ $t('companion.description') }}
                     </p>
                 </div>
             </div>
         </div>
-
-        <ul class="nav nav-tabs mt-2" role="tablist">
-            <li class="nav-item">
-                <a class="nav-link active" id="tab-qrcode" href="#companion-qrcode" role="tab" aria-controls="companion-qrcode" aria-selected="true" @click="$root.tab">
-                    {{ $t('companion.pair-new') }}
-                </a>
-            </li>
-            <li class="nav-item">
-                <a class="nav-link" id="tab-list" href="#companion-list" role="tab" aria-controls="companion-list" aria-selected="false" @click="$root.tab">
-                    {{ $t('companion.paired') }}
-                </a>
-            </li>
-        </ul>
-
+        <tabs class="mt-3" :tabs="['companion.pair-new','companion.paired']"></tabs>
         <div class="tab-content">
-            <div class="tab-pane active" id="companion-qrcode" role="tabpanel" aria-labelledby="tab-qrcode">
+            <div class="tab-pane active" id="new" role="tabpanel" aria-labelledby="tab-new">
                 <div class="card-body">
                     <div class="card-text">
                         <p>
                             {{ $t('companion.pairing-instructions') }}
                         </p>
                         <p>
                             <img :src="qrcode" />
                         </p>
                     </div>
                 </div>
             </div>
-            <div class="tab-pane" id="companion-list" role="tabpanel" aria-labelledby="tab-list">
+            <div class="tab-pane" id="paired" role="tabpanel" aria-labelledby="tab-paired">
                 <div class="card-body">
                     <companionapp-list class="card-text"></companionapp-list>
                 </div>
             </div>
         </div>
-
     </div>
 </template>
 
 <script>
     import CompanionappList from './Widgets/CompanionappList'
 
     export default {
         components: {
             CompanionappList
         },
         data() {
             return {
                 qrcode: ""
             }
         },
         mounted() {
             axios.get('/api/v4/companion/pairing', { loading: true })
                 .then(response => {
                     this.qrcode = response.data.qrcode
                 })
                 .catch(this.$root.errorHandler)
         }
     }
 </script>
diff --git a/src/resources/vue/Distlist/Info.vue b/src/resources/vue/Distlist/Info.vue
index 55e2f1cd..9b7609e5 100644
--- a/src/resources/vue/Distlist/Info.vue
+++ b/src/resources/vue/Distlist/Info.vue
@@ -1,148 +1,137 @@
 <template>
     <div class="container">
         <status-component v-if="list_id !== 'new'" :status="status" @status-update="statusUpdate"></status-component>
 
         <div class="card" id="distlist-info">
             <div class="card-body">
                 <div class="card-title" v-if="list_id !== 'new'">
                     {{ $tc('distlist.list-title', 1) }}
                     <btn class="btn-outline-danger button-delete float-end" @click="deleteList()" icon="trash-can">{{ $t('distlist.delete') }}</btn>
                 </div>
-                <div class="card-title" v-if="list_id === 'new'">{{ $t('distlist.new') }}</div>
+                <div class="card-title" v-else>{{ $t('distlist.new') }}</div>
                 <div class="card-text">
-                    <ul class="nav nav-tabs mt-3" role="tablist">
-                        <li class="nav-item">
-                            <a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
-                                {{ $t('form.general') }}
-                            </a>
-                        </li>
-                        <li v-if="list_id !== 'new'" class="nav-item">
-                            <a class="nav-link" id="tab-settings" href="#settings" role="tab" aria-controls="settings" aria-selected="false" @click="$root.tab">
-                                {{ $t('form.settings') }}
-                            </a>
-                        </li>
-                    </ul>
+                    <tabs class="mt-3" :tabs="list_id === 'new' ? ['form.general'] : ['form.general','form.settings']"></tabs>
                     <div class="tab-content">
                         <div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
                             <form @submit.prevent="submit" class="card-body">
                                 <div v-if="list_id !== 'new'" class="row plaintext mb-3">
                                     <label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
                                     <div class="col-sm-8">
                                         <span :class="$root.statusClass(list) + ' form-control-plaintext'" id="status">{{ $root.statusText(list) }}</span>
                                     </div>
                                 </div>
                                 <div class="row mb-3">
                                     <label for="name" class="col-sm-4 col-form-label">{{ $t('distlist.name') }}</label>
                                     <div class="col-sm-8">
                                         <input type="text" class="form-control" id="name" required v-model="list.name">
                                     </div>
                                 </div>
                                 <div class="row mb-3">
                                     <label for="email" class="col-sm-4 col-form-label">{{ $t('form.email') }}</label>
                                     <div class="col-sm-8">
                                         <input type="text" class="form-control" id="email" :disabled="list_id !== 'new'" required v-model="list.email">
                                     </div>
                                 </div>
                                 <div class="row mb-3">
                                     <label for="members-input" class="col-sm-4 col-form-label">{{ $t('distlist.recipients') }}</label>
                                     <div class="col-sm-8">
                                         <list-input id="members" :list="list.members"></list-input>
                                     </div>
                                 </div>
                                 <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
                             </form>
                         </div>
                         <div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
                             <form @submit.prevent="submitSettings" class="card-body">
                                 <div class="row mb-3">
                                     <label for="sender-policy-input" class="col-sm-4 col-form-label">{{ $t('distlist.sender-policy') }}</label>
                                     <div class="col-sm-8 pt-2">
                                         <list-input id="sender-policy" :list="list.config.sender_policy" class="mb-1"></list-input>
                                         <small id="sender-policy-hint" class="text-muted">
                                             {{ $t('distlist.sender-policy-text') }}
                                         </small>
                                     </div>
                                 </div>
                                 <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
                             </form>
                         </div>
                     </div>
                 </div>
             </div>
         </div>
     </div>
 </template>
 
 <script>
     import ListInput from '../Widgets/ListInput'
     import StatusComponent from '../Widgets/Status'
 
     export default {
         components: {
             ListInput,
             StatusComponent
         },
         data() {
             return {
                 list_id: null,
                 list: { members: [], config: {} },
                 status: {}
             }
         },
         created() {
             this.list_id = this.$route.params.list
 
             if (this.list_id != 'new') {
                 axios.get('/api/v4/groups/' + this.list_id, { loader: true })
                     .then(response => {
                         this.list = response.data
                         this.status = response.data.statusInfo
                     })
                     .catch(this.$root.errorHandler)
             }
         },
         mounted() {
             $('#name').focus()
         },
         methods: {
             deleteList() {
                 axios.delete('/api/v4/groups/' + this.list_id)
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.$router.push({ name: 'distlists' })
                         }
                     })
             },
             statusUpdate(list) {
                 this.list = Object.assign({}, this.list, list)
             },
             submit() {
                 this.$root.clearFormValidation($('#list-info form'))
 
                 let method = 'post'
                 let location = '/api/v4/groups'
 
                 if (this.list_id !== 'new') {
                     method = 'put'
                     location += '/' + this.list_id
                 }
 
                 axios[method](location, this.list)
                     .then(response => {
                         this.$toast.success(response.data.message)
                         this.$router.push({ name: 'distlists' })
                     })
             },
             submitSettings() {
                 this.$root.clearFormValidation($('#settings form'))
                 let post = this.list.config
 
                 axios.post('/api/v4/groups/' + this.list_id + '/config', post)
                     .then(response => {
                         this.$toast.success(response.data.message)
                     })
             }
         }
     }
 </script>
diff --git a/src/resources/vue/Domain/Info.vue b/src/resources/vue/Domain/Info.vue
index 1fa54420..56b6c245 100644
--- a/src/resources/vue/Domain/Info.vue
+++ b/src/resources/vue/Domain/Info.vue
@@ -1,207 +1,196 @@
 <template>
     <div class="container">
         <status-component v-if="domain_id !== 'new'" :status="status" @status-update="statusUpdate"></status-component>
 
         <div class="card">
             <div class="card-body">
                 <div class="card-title" v-if="domain_id === 'new'">{{ $t('domain.new') }}</div>
                 <div class="card-title" v-else>{{ $t('form.domain') }}
                     <btn class="btn-outline-danger button-delete float-end" @click="$refs.deleteDialog.show()" icon="trash-can">{{ $t('domain.delete') }}</btn>
                 </div>
                 <div class="card-text">
-                    <ul class="nav nav-tabs mt-3" role="tablist">
-                        <li class="nav-item">
-                            <a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
-                                {{ $t('form.general') }}
-                            </a>
-                        </li>
-                        <li class="nav-item" v-if="domain.id">
-                            <a class="nav-link" id="tab-settings" href="#settings" role="tab" aria-controls="settings" aria-selected="false" @click="$root.tab">
-                                {{ $t('form.settings') }}
-                            </a>
-                        </li>
-                    </ul>
+                    <tabs class="mt-3" :tabs="domain_id === 'new' ? ['form.general'] : ['form.general','form.settings']"></tabs>
                     <div class="tab-content">
                         <div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
                             <form @submit.prevent="submit" class="card-body">
                                 <div v-if="domain.id" class="row plaintext mb-3">
                                     <label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
                                     <div class="col-sm-8">
                                         <span :class="$root.statusClass(domain) + ' form-control-plaintext'" id="status">{{ $root.statusText(domain) }}</span>
                                     </div>
                                 </div>
                                 <div class="row mb-3">
                                     <label for="name" class="col-sm-4 col-form-label">{{ $t('domain.namespace') }}</label>
                                     <div class="col-sm-8">
                                         <input type="text" class="form-control" id="namespace" v-model="domain.namespace" :disabled="domain.id">
                                     </div>
                                 </div>
                                 <div v-if="!domain.id" id="domain-packages" class="row">
                                     <label class="col-sm-4 col-form-label">{{ $t('user.package') }}</label>
                                     <package-select class="col-sm-8 pt-sm-1" type="domain"></package-select>
                                 </div>
                                 <div v-if="domain.id" id="domain-skus" class="row">
                                     <label class="col-sm-4 col-form-label">{{ $t('user.subscriptions') }}</label>
                                     <subscription-select v-if="domain.id" class="col-sm-8 pt-sm-1" type="domain" :object="domain" :readonly="true"></subscription-select>
                                 </div>
                                 <btn v-if="!domain.id" class="btn-primary mt-3" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
                             </form>
                             <hr class="m-0" v-if="domain.id">
                             <div v-if="domain.id && !domain.isConfirmed" class="card-body" id="domain-verify">
                                 <h5 class="mb-3">{{ $t('domain.verify') }}</h5>
                                 <div class="card-text">
                                     <p>{{ $t('domain.verify-intro') }}</p>
                                     <p>
                                         <span v-html="$t('domain.verify-dns')"></span>
                                         <ul>
                                             <li>{{ $t('domain.verify-dns-txt') }} <code>{{ domain.hash_text }}</code></li>
                                             <li>{{ $t('domain.verify-dns-cname') }} <code>{{ domain.hash_cname }}.{{ domain.namespace }}. IN CNAME {{ domain.hash_code }}.{{ domain.namespace }}.</code></li>
                                         </ul>
                                         <span>{{ $t('domain.verify-outro') }}</span>
                                     </p>
                                     <p>{{ $t('domain.verify-sample') }} <pre>{{ domain.dns.join("\n") }}</pre></p>
                                     <btn class="btn-primary" @click="confirm" icon="rotate">{{ $t('btn.verify') }}</btn>
                                 </div>
                             </div>
                             <div v-if="domain.isConfirmed" class="card-body" id="domain-config">
                                 <h5 class="mb-3">{{ $t('domain.config') }}</h5>
                                 <div class="card-text">
                                     <p>{{ $t('domain.config-intro', { app: $root.appName }) }}</p>
                                     <p>{{ $t('domain.config-sample') }} <pre>{{ domain.mx.join("\n") }}</pre></p>
                                     <p>{{ $t('domain.config-hint') }}</p>
                                 </div>
                             </div>
                         </div>
                         <div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
                             <div class="card-body">
                                 <form @submit.prevent="submitSettings">
                                     <div class="row mb-3">
                                         <label for="spf_whitelist" class="col-sm-4 col-form-label">{{ $t('domain.spf-whitelist') }}</label>
                                         <div class="col-sm-8">
                                             <list-input id="spf_whitelist" name="spf_whitelist" :list="spf_whitelist"></list-input>
                                             <small id="spf-hint" class="text-muted d-block mt-2">
                                                 {{ $t('domain.spf-whitelist-text') }}
                                                 <span class="d-block" v-html="$t('domain.spf-whitelist-ex')"></span>
                                             </small>
                                         </div>
                                     </div>
                                     <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
                                 </form>
                             </div>
                         </div>
                     </div>
                 </div>
             </div>
         </div>
 
         <modal-dialog id="delete-warning" ref="deleteDialog" @click="deleteDomain()" :buttons="['delete']" :cancel-focus="true"
                       :title="$t('domain.delete-domain', { domain: domain.namespace })"
         >
             <p>{{ $t('domain.delete-text') }}</p>
         </modal-dialog>
     </div>
 </template>
 
 <script>
     import ListInput from '../Widgets/ListInput'
     import ModalDialog from '../Widgets/ModalDialog'
     import PackageSelect from '../Widgets/PackageSelect'
     import StatusComponent from '../Widgets/Status'
     import SubscriptionSelect from '../Widgets/SubscriptionSelect'
 
     import { library } from '@fortawesome/fontawesome-svg-core'
 
     library.add(
         require('@fortawesome/free-solid-svg-icons/faRotate').definition,
     )
 
     export default {
         components: {
             ListInput,
             ModalDialog,
             PackageSelect,
             StatusComponent,
             SubscriptionSelect
         },
         data() {
             return {
                 domain_id: null,
                 domain: {},
                 spf_whitelist: [],
                 status: {}
             }
         },
         created() {
             this.domain_id = this.$route.params.domain
 
             if (this.domain_id !== 'new') {
                 axios.get('/api/v4/domains/' + this.domain_id, { loader: true })
                     .then(response => {
                         this.domain = response.data
                         this.spf_whitelist = this.domain.config.spf_whitelist || []
 
                         if (!this.domain.isConfirmed) {
                             $('#domain-verify button').focus()
                         }
 
                         this.status = response.data.statusInfo
                     })
                     .catch(this.$root.errorHandler)
             }
         },
         mounted() {
             $('#namespace').focus()
         },
         methods: {
             confirm() {
                 axios.get('/api/v4/domains/' + this.domain_id + '/confirm')
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.domain.isConfirmed = true
                             this.status = response.data.statusInfo
                         }
 
                         if (response.data.message) {
                             this.$toast[response.data.status](response.data.message)
                         }
                     })
             },
             deleteDomain() {
                 // Delete the domain from the confirm dialog
                 axios.delete('/api/v4/domains/' + this.domain_id)
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.$router.push({ name: 'domains' })
                         }
                     })
             },
             statusUpdate(domain) {
                 this.domain = Object.assign({}, this.domain, domain)
             },
             submit() {
                 this.$root.clearFormValidation($('#general form'))
 
                 let post = this.$root.pick(this.domain, ['namespace'])
 
                 post.package = $('#domain-packages input:checked').val()
 
                 axios.post('/api/v4/domains', post)
                     .then(response => {
                         this.$toast.success(response.data.message)
                         this.$router.push({ name: 'domains' })
                     })
             },
             submitSettings() {
                 this.$root.clearFormValidation($('#settings form'))
 
                 const post = this.$root.pick(this, ['spf_whitelist'])
 
                 axios.post('/api/v4/domains/' + this.domain_id + '/config', post)
                     .then(response => {
                         this.$toast.success(response.data.message)
                     })
             }
         }
     }
 </script>
diff --git a/src/resources/vue/File/Info.vue b/src/resources/vue/File/Info.vue
index 3b8c6c1d..ed56ed72 100644
--- a/src/resources/vue/File/Info.vue
+++ b/src/resources/vue/File/Info.vue
@@ -1,162 +1,151 @@
 <template>
     <div class="container">
         <div class="card" id="file-info">
             <div class="card-body">
                 <div class="card-title">
                     {{ file.name }}
                     <btn v-if="file.canDelete" class="btn-outline-danger button-delete float-end" @click="fileDelete" icon="trash-can">{{ $t('file.delete') }}</btn>
                 </div>
                 <div class="card-text">
-                    <ul class="nav nav-tabs mt-3" role="tablist">
-                        <li class="nav-item">
-                            <a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
-                                {{ $t('form.general') }}
-                            </a>
-                        </li>
-                        <li class="nav-item" v-if="file.isOwner">
-                            <a class="nav-link" id="tab-sharing" href="#sharing" role="tab" aria-controls="sharing" aria-selected="false" @click="$root.tab">
-                                {{ $t('file.sharing') }}
-                            </a>
-                        </li>
-                    </ul>
+                    <tabs class="mt-3" :tabs="file.isOwner ? ['form.general','file.sharing'] : ['form.general']"></tabs>
                     <div class="tab-content">
                         <form class="tab-pane show active card-body read-only short" id="general" role="tabpanel" aria-labelledby="tab-general">
                             <div class="row plaintext">
                                 <label for="mimetype" class="col-sm-4 col-form-label">{{ $t('file.mimetype') }}</label>
                                 <div class="col-sm-8">
                                     <span class="form-control-plaintext" id="mimetype">{{ file.mimetype }}</span>
                                 </div>
                             </div>
                             <div class="row plaintext">
                                 <label for="size" class="col-sm-4 col-form-label">{{ $t('form.size') }}</label>
                                 <div class="col-sm-8">
                                     <span class="form-control-plaintext" id="size">{{ api.sizeText(file.size) }}</span>
                                 </div>
                             </div>
                             <div class="row plaintext mb-3">
                                 <label for="mtime" class="col-sm-4 col-form-label">{{ $t('file.mtime') }}</label>
                                 <div class="col-sm-8">
                                     <span class="form-control-plaintext" id="mtime">{{ file.mtime }}</span>
                                 </div>
                             </div>
                             <btn class="btn-primary" icon="download" @click="fileDownload">{{ $t('btn.download') }}</btn>
                         </form>
                         <div v-if="file.isOwner" class="tab-pane card-body" id="sharing" role="tabpanel" aria-labelledby="tab-sharing">
                             <div id="share-form" class="mb-3">
                                 <div class="row">
                                     <small id="share-links-hint" class="text-muted mb-2">
                                         {{ $t('file.sharing-links-text') }}
                                     </small>
                                     <div class="input-group">
                                         <input type="text" class="form-control" id="user" :placeholder="$t('form.email')">
                                         <a href="#" class="btn btn-outline-secondary" @click.prevent="shareAdd">
                                             <svg-icon icon="plus"></svg-icon><span class="visually-hidden">{{ $t('btn.add') }}</span>
                                         </a>
                                     </div>
                                 </div>
                             </div>
                             <div id="share-links" class="row m-0" v-if="shares.length">
                                 <div class="list-group p-0">
                                     <div v-for="item in shares" :key="item.id" class="list-group-item">
                                         <div class="d-flex w-100 justify-content-between">
                                             <span class="user lh-lg">
                                                 <svg-icon icon="user"></svg-icon> {{ item.user }}
                                             </span>
                                             <span class="d-inline-block">
                                                 <btn class="btn-link p-1" :icon="['far', 'clipboard']" :title="$t('btn.copy')" @click="copyLink(item.link)"></btn>
                                                 <btn class="btn-link text-danger p-1" icon="trash-can" :title="$t('btn.delete')" @click="shareDelete(item.id)"></btn>
                                             </span>
                                         </div>
                                         <code>{{ item.link }}</code>
                                     </div>
                                 </div>
                             </div>
                         </div>
                     </div>
                 </div>
             </div>
         </div>
     </div>
 </template>
 
 <script>
     import FileAPI from '../../js/files.js'
 
     import { library } from '@fortawesome/fontawesome-svg-core'
 
     library.add(
         require('@fortawesome/free-regular-svg-icons/faClipboard').definition,
         require('@fortawesome/free-solid-svg-icons/faDownload').definition,
     )
 
     export default {
         data() {
             return {
                 file: {},
                 fileId: null,
                 shares: []
             }
         },
         created() {
             this.api = new FileAPI({})
 
             this.fileId = this.$route.params.file
 
             axios.get('/api/v4/files/' + this.fileId, { loader: true })
                 .then(response => {
                     this.file = response.data
 
                     if (this.file.isOwner) {
                         axios.get('api/v4/files/' + this.fileId + '/permissions')
                             .then(response => {
                                 if (response.data.list) {
                                     this.shares = response.data.list
                                 }
                             })
                     }
                 })
                 .catch(this.$root.errorHandler)
         },
         methods: {
             copyLink(link) {
                 navigator.clipboard.writeText(link);
             },
             fileDelete() {
                 axios.delete('api/v4/files/' + this.fileId)
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.$router.push({ name: 'files' })
                         }
                     })
             },
             fileDownload() {
                 this.api.fileDownload(this.fileId)
             },
             shareAdd() {
                 let post = { permissions: 'read-only', user: $('#user').val() }
 
                 if (!post.user) {
                     return
                 }
 
                 axios.post('api/v4/files/' + this.fileId + '/permissions', post)
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.shares.push(response.data)
                         }
                     })
             },
             shareDelete(id) {
                 axios.delete('api/v4/files/' + this.fileId + '/permissions/' + id)
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.$delete(this.shares, this.shares.findIndex(element => element.id == id))
                         }
                     })
             }
         }
     }
 </script>
diff --git a/src/resources/vue/Resource/Info.vue b/src/resources/vue/Resource/Info.vue
index 1858d648..c266500d 100644
--- a/src/resources/vue/Resource/Info.vue
+++ b/src/resources/vue/Resource/Info.vue
@@ -1,182 +1,171 @@
 <template>
     <div class="container">
         <status-component v-if="resource_id !== 'new'" :status="status" @status-update="statusUpdate"></status-component>
 
         <div class="card" id="resource-info">
             <div class="card-body">
                 <div class="card-title" v-if="resource_id !== 'new'">
                     {{ $tc('resource.list-title', 1) }}
                     <btn class="btn-outline-danger button-delete float-end" @click="deleteResource()" icon="trash-can">{{ $t('resource.delete') }}</btn>
                 </div>
                 <div class="card-title" v-if="resource_id === 'new'">{{ $t('resource.new') }}</div>
                 <div class="card-text">
-                    <ul class="nav nav-tabs mt-3" role="tablist">
-                        <li class="nav-item">
-                            <a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
-                                {{ $t('form.general') }}
-                            </a>
-                        </li>
-                        <li v-if="resource_id !== 'new'" class="nav-item">
-                            <a class="nav-link" id="tab-settings" href="#settings" role="tab" aria-controls="settings" aria-selected="false" @click="$root.tab">
-                                {{ $t('form.settings') }}
-                            </a>
-                        </li>
-                    </ul>
+                    <tabs class="mt-3" :tabs="resource_id === 'new' ? ['form.general'] : ['form.general','form.settings']"></tabs>
                     <div class="tab-content">
                         <div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
                             <form @submit.prevent="submit" class="card-body">
                                 <div v-if="resource_id !== 'new'" class="row plaintext mb-3">
                                     <label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
                                     <div class="col-sm-8">
                                         <span :class="$root.statusClass(resource) + ' form-control-plaintext'" id="status">{{ $root.statusText(resource) }}</span>
                                     </div>
                                 </div>
                                 <div class="row mb-3">
                                     <label for="name" class="col-sm-4 col-form-label">{{ $t('form.name') }}</label>
                                     <div class="col-sm-8">
                                         <input type="text" class="form-control" id="name" v-model="resource.name">
                                     </div>
                                 </div>
                                 <div v-if="domains.length" class="row mb-3">
                                     <label for="domain" class="col-sm-4 col-form-label">{{ $t('form.domain') }}</label>
                                     <div class="col-sm-8">
                                         <select class="form-select" v-model="resource.domain">
                                             <option v-for="_domain in domains" :key="_domain.id" :value="_domain.namespace">{{ _domain.namespace }}</option>
                                         </select>
                                     </div>
                                 </div>
                                 <div v-if="resource.email" class="row mb-3">
                                     <label for="email" class="col-sm-4 col-form-label">{{ $t('form.email') }}</label>
                                     <div class="col-sm-8">
                                         <input type="text" class="form-control" id="email" disabled v-model="resource.email">
                                     </div>
                                 </div>
                                 <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
                             </form>
                         </div>
                         <div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
                             <form @submit.prevent="submitSettings" class="card-body">
                                 <div class="row mb-3">
                                     <label for="invitation_policy" class="col-sm-4 col-form-label">{{ $t('resource.invitation-policy') }}</label>
                                     <div class="col-sm-8">
                                         <div class="input-group input-group-select mb-1">
                                             <select class="form-select" id="invitation_policy" v-model="resource.config.invitation_policy" @change="policyChange">
                                                 <option value="accept">{{ $t('resource.ipolicy-accept') }}</option>
                                                 <option value="manual">{{ $t('resource.ipolicy-manual') }}</option>
                                                 <option value="reject">{{ $t('resource.ipolicy-reject') }}</option>
                                             </select>
                                             <input type="text" class="form-control" id="owner" v-model="resource.config.owner" :placeholder="$t('form.email')">
                                         </div>
                                         <small id="invitation-policy-hint" class="text-muted">
                                             {{ $t('resource.invitation-policy-text') }}
                                         </small>
                                     </div>
                                 </div>
                                 <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
                             </form>
                         </div>
                     </div>
                 </div>
             </div>
         </div>
     </div>
 </template>
 
 <script>
     import StatusComponent from '../Widgets/Status'
 
     export default {
         components: {
             StatusComponent
         },
         data() {
             return {
                 domains: [],
                 resource_id: null,
                 resource: { config: {} },
                 status: {}
             }
         },
         created() {
             this.resource_id = this.$route.params.resource
 
             if (this.resource_id != 'new') {
                 axios.get('/api/v4/resources/' + this.resource_id, { loader: true })
                     .then(response => {
                         this.resource = response.data
                         this.status = response.data.statusInfo
 
                         if (this.resource.config.invitation_policy.match(/^manual:(.+)$/)) {
                             this.resource.config.owner = RegExp.$1
                             this.resource.config.invitation_policy = 'manual'
                         }
                         this.$nextTick().then(() => { this.policyChange() })
                     })
                     .catch(this.$root.errorHandler)
             } else {
                 axios.get('/api/v4/domains', { loader: true })
                     .then(response => {
                         this.domains = response.data.list
                         this.resource.domain = this.domains[0].namespace
                     })
                     .catch(this.$root.errorHandler)
             }
         },
         mounted() {
             $('#name').focus()
         },
         methods: {
             deleteResource() {
                 axios.delete('/api/v4/resources/' + this.resource_id)
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.$router.push({ name: 'resources' })
                         }
                     })
             },
             policyChange() {
                 let select = $('#invitation_policy')
                 select.parent()[select.val() == 'manual' ? 'addClass' : 'removeClass']('selected')
             },
             statusUpdate(resource) {
                 this.resource = Object.assign({}, this.resource, resource)
             },
             submit() {
                 this.$root.clearFormValidation($('#resource-info form'))
 
                 let method = 'post'
                 let location = '/api/v4/resources'
 
                 if (this.resource_id !== 'new') {
                     method = 'put'
                     location += '/' + this.resource_id
                 }
 
                 const post = this.$root.pick(this.resource, ['id', 'name', 'domain'])
 
                 axios[method](location, post)
                     .then(response => {
                         this.$toast.success(response.data.message)
                         this.$router.push({ name: 'resources' })
                     })
             },
             submitSettings() {
                 this.$root.clearFormValidation($('#settings form'))
 
                 let post = this.$root.pick(this.resource.config, ['invitation_policy', 'owner'])
 
                 if (post.invitation_policy == 'manual') {
                     post.invitation_policy += ':' + post.owner
                 }
 
                 delete post.owner
 
                 axios.post('/api/v4/resources/' + this.resource_id + '/config', post)
                     .then(response => {
                         this.$toast.success(response.data.message)
                     })
             }
         }
     }
 </script>
diff --git a/src/resources/vue/SharedFolder/Info.vue b/src/resources/vue/SharedFolder/Info.vue
index 69c33895..b52a8cc9 100644
--- a/src/resources/vue/SharedFolder/Info.vue
+++ b/src/resources/vue/SharedFolder/Info.vue
@@ -1,176 +1,165 @@
 <template>
     <div class="container">
         <status-component v-if="folder_id !== 'new'" :status="status" @status-update="statusUpdate"></status-component>
 
         <div class="card" id="folder-info">
             <div class="card-body">
                 <div class="card-title" v-if="folder_id !== 'new'">
                     {{ $tc('shf.list-title', 1) }}
                     <btn class="btn-outline-danger button-delete float-end" @click="deleteFolder()" icon="trash-can">{{ $t('shf.delete') }}</btn>
                 </div>
                 <div class="card-title" v-if="folder_id === 'new'">{{ $t('shf.new') }}</div>
                 <div class="card-text">
-                    <ul class="nav nav-tabs mt-3" role="tablist">
-                        <li class="nav-item">
-                            <a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
-                                {{ $t('form.general') }}
-                            </a>
-                        </li>
-                        <li v-if="folder_id !== 'new'" class="nav-item">
-                            <a class="nav-link" id="tab-settings" href="#settings" role="tab" aria-controls="settings" aria-selected="false" @click="$root.tab">
-                                {{ $t('form.settings') }}
-                            </a>
-                        </li>
-                    </ul>
+                    <tabs class="mt-3" :tabs="folder_id === 'new' ? ['form.general'] : ['form.general', 'form.settings']"></tabs>
                     <div class="tab-content">
                         <div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
                             <form @submit.prevent="submit" class="card-body">
                                 <div v-if="folder_id !== 'new'" class="row plaintext mb-3">
                                     <label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
                                     <div class="col-sm-8">
                                         <span :class="$root.statusClass(folder) + ' form-control-plaintext'" id="status">{{ $root.statusText(folder) }}</span>
                                     </div>
                                 </div>
                                 <div class="row mb-3">
                                     <label for="name" class="col-sm-4 col-form-label">{{ $t('form.name') }}</label>
                                     <div class="col-sm-8">
                                         <input type="text" class="form-control" id="name" v-model="folder.name">
                                     </div>
                                 </div>
                                 <div class="row mb-3">
                                     <label for="type" class="col-sm-4 col-form-label">{{ $t('form.type') }}</label>
                                     <div class="col-sm-8">
                                         <select id="type" class="form-select" v-model="folder.type" :disabled="folder_id !== 'new'">
                                             <option v-for="type in types" :key="type" :value="type">{{ $t('shf.type-' + type) }}</option>
                                         </select>
                                     </div>
                                 </div>
                                 <div v-if="domains.length" class="row mb-3">
                                     <label for="domain" class="col-sm-4 col-form-label">{{ $t('form.domain') }}</label>
                                     <div v-if="domains.length" class="col-sm-8">
                                         <select class="form-select" v-model="folder.domain">
                                             <option v-for="_domain in domains" :key="_domain.id" :value="_domain.namespace">{{ _domain.namespace }}</option>
                                         </select>
                                     </div>
                                 </div>
                                 <div class="row mb-3" v-if="folder.type == 'mail'">
                                     <label for="aliases-input" class="col-sm-4 col-form-label">{{ $t('form.emails') }}</label>
                                     <div class="col-sm-8">
                                         <list-input id="aliases" :list="folder.aliases"></list-input>
                                     </div>
                                 </div>
                                 <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
                             </form>
                         </div>
                         <div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
                             <form @submit.prevent="submitSettings" class="card-body">
                                 <div class="row mb-3">
                                     <label for="acl-input" class="col-sm-4 col-form-label">{{ $t('form.acl') }}</label>
                                     <div class="col-sm-8">
                                         <acl-input id="acl" v-model="folder.config.acl" :list="folder.config.acl" class="mb-1"></acl-input>
                                         <small id="acl-hint" class="text-muted">
                                             {{ $t('shf.acl-text') }}
                                         </small>
                                     </div>
                                 </div>
                                 <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
                             </form>
                         </div>
                     </div>
                 </div>
             </div>
         </div>
     </div>
 </template>
 
 <script>
     import AclInput from '../Widgets/AclInput'
     import ListInput from '../Widgets/ListInput'
     import StatusComponent from '../Widgets/Status'
 
     export default {
         components: {
             AclInput,
             ListInput,
             StatusComponent
         },
         data() {
             return {
                 domains: [],
                 folder_id: null,
                 folder: { type: 'mail', config: {}, aliases: [] },
                 status: {},
                 types: [ 'mail', 'event', 'task', 'contact', 'note', 'file' ]
             }
         },
         created() {
             this.folder_id = this.$route.params.folder
 
             if (this.folder_id != 'new') {
                 axios.get('/api/v4/shared-folders/' + this.folder_id, { loader: true })
                     .then(response => {
                         this.folder = response.data
                         this.status = response.data.statusInfo
                     })
                     .catch(this.$root.errorHandler)
             } else {
                 axios.get('/api/v4/domains', { loader: true })
                     .then(response => {
                         this.domains = response.data.list
                         this.folder.domain = this.domains[0].namespace
                     })
                     .catch(this.$root.errorHandler)
             }
         },
         mounted() {
             $('#name').focus()
         },
         methods: {
             deleteFolder() {
                 axios.delete('/api/v4/shared-folders/' + this.folder_id)
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.$router.push({ name: 'shared-folders' })
                         }
                     })
             },
             statusUpdate(folder) {
                 this.folder = Object.assign({}, this.folder, folder)
             },
             submit() {
                 this.$root.clearFormValidation($('#folder-info form'))
 
                 let method = 'post'
                 let location = '/api/v4/shared-folders'
 
                 if (this.folder_id !== 'new') {
                     method = 'put'
                     location += '/' + this.folder_id
                 }
 
                 const post = this.$root.pick(this.folder, ['id', 'name', 'domain', 'type', 'aliases'])
 
                 if (post.type != 'mail') {
                     delete post.aliases
                 }
 
                 axios[method](location, post)
                     .then(response => {
                         this.$toast.success(response.data.message)
                         this.$router.push({ name: 'shared-folders' })
                     })
             },
             submitSettings() {
                 this.$root.clearFormValidation($('#settings form'))
 
                 let post = this.$root.pick(this.folder.config, ['acl'])
 
                 axios.post('/api/v4/shared-folders/' + this.folder_id + '/config', post)
                     .then(response => {
                         this.$toast.success(response.data.message)
                     })
             }
         }
     }
 </script>
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
index 1ea85ed6..ea340ece 100644
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -1,308 +1,297 @@
 <template>
     <div class="container">
         <status-component v-if="user_id !== 'new'" :status="status" @status-update="statusUpdate"></status-component>
 
         <div class="card" id="user-info">
             <div class="card-body">
                 <div class="card-title" v-if="user_id !== 'new'">{{ $t('user.title') }}
                     <btn icon="trash-can" class="btn-outline-danger button-delete float-end" @click="showDeleteConfirmation()">
                         {{ $t('user.delete') }}
                     </btn>
                 </div>
                 <div class="card-title" v-if="user_id === 'new'">{{ $t('user.new') }}</div>
                 <div class="card-text">
-                    <ul class="nav nav-tabs mt-3" role="tablist">
-                        <li class="nav-item">
-                            <a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
-                                {{ $t('form.general') }}
-                            </a>
-                        </li>
-                        <li v-if="user_id !== 'new'" class="nav-item">
-                            <a class="nav-link" id="tab-settings" href="#settings" role="tab" aria-controls="settings" aria-selected="false" @click="$root.tab">
-                                {{ $t('form.settings') }}
-                            </a>
-                        </li>
-                    </ul>
+                    <tabs class="mt-3" :tabs="user_id === 'new' ? ['form.general'] : ['form.general','form.settings']"></tabs>
                     <div class="tab-content">
                         <div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
                             <form @submit.prevent="submit" class="card-body">
                                 <div v-if="user_id !== 'new'" class="row plaintext mb-3">
                                     <label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
                                     <div class="col-sm-8">
                                         <span :class="$root.statusClass(user) + ' form-control-plaintext'" id="status">{{ $root.statusText(user) }}</span>
                                     </div>
                                 </div>
                                 <div class="row mb-3">
                                     <label for="first_name" class="col-sm-4 col-form-label">{{ $t('form.firstname') }}</label>
                                     <div class="col-sm-8">
                                         <input type="text" class="form-control" id="first_name" v-model="user.first_name">
                                     </div>
                                 </div>
                                 <div class="row mb-3">
                                     <label for="last_name" class="col-sm-4 col-form-label">{{ $t('form.lastname') }}</label>
                                     <div class="col-sm-8">
                                         <input type="text" class="form-control" id="last_name" v-model="user.last_name">
                                     </div>
                                 </div>
                                 <div class="row mb-3">
                                     <label for="organization" class="col-sm-4 col-form-label">{{ $t('user.org') }}</label>
                                     <div class="col-sm-8">
                                         <input type="text" class="form-control" id="organization" v-model="user.organization">
                                     </div>
                                 </div>
                                 <div class="row mb-3">
                                     <label for="email" class="col-sm-4 col-form-label">{{ $t('form.email') }}</label>
                                     <div class="col-sm-8">
                                         <input type="text" class="form-control" id="email" :disabled="user_id !== 'new'" required v-model="user.email">
                                     </div>
                                 </div>
                                 <div class="row mb-3">
-                                    <label for="aliases-input" class="col-sm-4 col-form-label">{{ $t('user.aliases-email') }}</label>
+                                    <label for="aliases-input" class="col-sm-4 col-form-label">{{ $t('user.email-aliases') }}</label>
                                     <div class="col-sm-8">
                                         <list-input id="aliases" :list="user.aliases"></list-input>
                                     </div>
                                 </div>
                                 <div class="row mb-3">
                                     <label for="password" class="col-sm-4 col-form-label">{{ $t('form.password') }}</label>
                                     <div class="col-sm-8">
                                         <div v-if="!isSelf" class="btn-group w-100" role="group">
                                             <input type="checkbox" id="pass-mode-input" value="input" class="btn-check" @change="setPasswordMode" :checked="passwordMode == 'input'">
                                             <label class="btn btn-outline-secondary" for="pass-mode-input">{{ $t('user.pass-input') }}</label>
                                             <input type="checkbox" id="pass-mode-link" value="link" class="btn-check" @change="setPasswordMode">
                                             <label class="btn btn-outline-secondary" for="pass-mode-link">{{ $t('user.pass-link') }}</label>
                                         </div>
                                         <password-input v-if="passwordMode == 'input'" :class="isSelf ? '' : 'mt-2'" v-model="user"></password-input>
                                         <div id="password-link" v-if="passwordMode == 'link' || user.passwordLinkCode" class="mt-2">
                                             <span>{{ $t('user.pass-link-label') }}</span>&nbsp;<code>{{ passwordLink }}</code>
                                             <span class="d-inline-block">
                                                 <btn class="btn-link p-1" :icon="['far', 'clipboard']" :title="$t('btn.copy')" @click="passwordLinkCopy"></btn>
                                                 <btn v-if="user.passwordLinkCode" class="btn-link text-danger p-1" icon="trash-can" :title="$t('btn.delete')" @click="passwordLinkDelete"></btn>
                                             </span>
                                             <div v-if="!user.passwordLinkCode" class="form-text m-0">{{ $t('user.pass-link-hint') }}</div>
                                         </div>
                                     </div>
                                 </div>
                                 <div v-if="user_id === 'new'" id="user-packages" class="row mb-3">
                                     <label class="col-sm-4 col-form-label">{{ $t('user.package') }}</label>
                                     <package-select class="col-sm-8 pt-sm-1"></package-select>
                                 </div>
                                 <div v-if="user_id !== 'new'" id="user-skus" class="row mb-3">
                                     <label class="col-sm-4 col-form-label">{{ $t('user.subscriptions') }}</label>
                                     <subscription-select v-if="user.id" class="col-sm-8 pt-sm-1" :object="user"></subscription-select>
                                 </div>
                                 <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
                             </form>
                         </div>
                         <div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
                             <form @submit.prevent="submitSettings" class="card-body">
                                 <div class="row checkbox mb-3">
                                     <label for="greylist_enabled" class="col-sm-4 col-form-label">{{ $t('user.greylisting') }}</label>
                                     <div class="col-sm-8 pt-2">
                                         <input type="checkbox" id="greylist_enabled" name="greylist_enabled" value="1" class="form-check-input d-block mb-2" :checked="user.config.greylist_enabled">
                                         <small id="greylisting-hint" class="text-muted">
                                             {{ $t('user.greylisting-text') }}
                                         </small>
                                     </div>
                                 </div>
                                 <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
                             </form>
                         </div>
                     </div>
                 </div>
             </div>
         </div>
         <modal-dialog id="delete-warning" ref="deleteWarning" :buttons="['delete']" :cancel-focus="true" @click="deleteUser()"
                       :title="$t('user.delete-email', { email: user.email })"
         >
             <p>{{ $t('user.delete-text') }}</p>
         </modal-dialog>
     </div>
 </template>
 
 <script>
     import ListInput from '../Widgets/ListInput'
     import ModalDialog from '../Widgets/ModalDialog'
     import PackageSelect from '../Widgets/PackageSelect'
     import PasswordInput from '../Widgets/PasswordInput'
     import StatusComponent from '../Widgets/Status'
     import SubscriptionSelect from '../Widgets/SubscriptionSelect'
 
     import { library } from '@fortawesome/fontawesome-svg-core'
 
     library.add(
         require('@fortawesome/free-regular-svg-icons/faClipboard').definition,
     )
 
     export default {
         components: {
             ListInput,
             ModalDialog,
             PackageSelect,
             PasswordInput,
             StatusComponent,
             SubscriptionSelect
         },
         data() {
             return {
                 passwordLinkCode: '',
                 passwordMode: '',
                 user_id: null,
                 user: { aliases: [], config: [] },
                 status: {}
             }
         },
         computed: {
             isSelf: function () {
                 return this.user_id == this.$root.authInfo.id
             },
             passwordLink: function () {
                 return this.$root.appUrl + '/password-reset/' + this.passwordLinkCode
             }
         },
         created() {
             this.user_id = this.$route.params.user
 
             if (this.user_id !== 'new') {
                 axios.get('/api/v4/users/' + this.user_id, { loader: true })
                     .then(response => {
                         this.user = response.data
                         this.user.first_name = response.data.settings.first_name
                         this.user.last_name = response.data.settings.last_name
                         this.user.organization = response.data.settings.organization
                         this.status = response.data.statusInfo
 
                         this.passwordLinkCode = this.user.passwordLinkCode
                     })
                     .catch(this.$root.errorHandler)
 
                 if (this.isSelf) {
                     this.passwordMode = 'input'
                 }
             } else {
                 this.passwordMode = 'input'
             }
         },
         mounted() {
             $('#first_name').focus()
         },
         methods: {
             passwordLinkCopy() {
                 navigator.clipboard.writeText($('#password-link code').text());
             },
             passwordLinkDelete() {
                 this.passwordMode = ''
                 $('#pass-mode-link')[0].checked = false
 
                 // Delete the code for real
                 axios.delete('/api/v4/password-reset/code/' + this.passwordLinkCode)
                     .then(response => {
                         this.passwordLinkCode = ''
                         this.user.passwordLinkCode = ''
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                         }
                     })
             },
             setPasswordMode(event) {
                 const mode = event.target.checked ? event.target.value : ''
 
                 // In the "new user" mode the password mode cannot be unchecked
                 if (!mode && this.user_id === 'new') {
                     event.target.checked = true
                     return
                 }
 
                 this.passwordMode = mode
 
                 if (!event.target.checked) {
                     return
                 }
 
                 $('#pass-mode-' + (mode == 'link' ? 'input' : 'link'))[0].checked = false
 
                 // Note: we use $nextTick() because we have to wait for the HTML elements to exist
                 this.$nextTick().then(() => {
                     if (mode == 'link' && !this.passwordLinkCode) {
                         axios.post('/api/v4/password-reset/code', {}, { loader: '#password-link' })
                             .then(response => {
                                 this.passwordLinkCode = response.data.short_code + '-' + response.data.code
                             })
                     } else if (mode == 'input') {
                         $('#password').focus();
                     }
                 })
             },
             submit() {
                 this.$root.clearFormValidation($('#general form'))
 
                 let method = 'post'
                 let location = '/api/v4/users'
                 let post = this.$root.pick(this.user, ['aliases', 'email', 'first_name', 'last_name', 'organization'])
 
                 if (this.user_id !== 'new') {
                     method = 'put'
                     location += '/' + this.user_id
 
                     let skus = {}
                     $('#user-skus input[type=checkbox]:checked').each((idx, input) => {
                         let id = $(input).val()
                         let range = $(input).parents('tr').first().find('input[type=range]').val()
 
                         skus[id] = range || 1
                     })
                     post.skus = skus
                 } else {
                     post.package = $('#user-packages input:checked').val()
                 }
 
                 if (this.passwordMode == 'link' && this.passwordLinkCode) {
                     post.passwordLinkCode = this.passwordLinkCode
                 } else if (this.passwordMode == 'input') {
                     post.password = this.user.password
                     post.password_confirmation = this.user.password_confirmation
                 }
 
                 axios[method](location, post)
                     .then(response => {
                         if (response.data.statusInfo) {
                             this.$root.authInfo.statusInfo = response.data.statusInfo
                         }
 
                         this.$toast.success(response.data.message)
                         this.$router.push({ name: 'users' })
                     })
             },
             submitSettings() {
                 this.$root.clearFormValidation($('#settings form'))
                 let post = { greylist_enabled: $('#greylist_enabled').prop('checked') ? 1 : 0 }
 
                 axios.post('/api/v4/users/' + this.user_id + '/config', post)
                     .then(response => {
                         this.$toast.success(response.data.message)
                     })
             },
             statusUpdate(user) {
                 this.user = Object.assign({}, this.user, user)
             },
             deleteUser() {
                 // Delete the user from the confirm dialog
                 axios.delete('/api/v4/users/' + this.user_id)
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.$router.push({ name: 'users' })
                         }
                     })
             },
             showDeleteConfirmation() {
                 if (this.user_id == this.$root.authInfo.id) {
                     // Deleting self, redirect to /profile/delete page
                     this.$router.push({ name: 'profile-delete' })
                 } else {
                     // Display the warning
                     this.$refs.deleteWarning.show()
                 }
             }
         }
     }
 </script>
diff --git a/src/resources/vue/Wallet.vue b/src/resources/vue/Wallet.vue
index abc7ecb5..8f798197 100644
--- a/src/resources/vue/Wallet.vue
+++ b/src/resources/vue/Wallet.vue
@@ -1,426 +1,410 @@
 <template>
     <div class="container" dusk="wallet-component">
         <div v-if="wallet.id" id="wallet" class="card">
             <div class="card-body">
                 <div class="card-title">{{ $t('wallet.title') }} <span :class="wallet.balance < 0 ? 'text-danger' : 'text-success'">{{ $root.price(wallet.balance, wallet.currency) }}</span></div>
                 <div class="card-text">
                     <p v-if="wallet.notice" id="wallet-notice">{{ wallet.notice }}</p>
 
                     <div v-if="showPendingPayments" class="alert alert-warning">
                         {{ $t('wallet.pending-payments-warning') }}
                     </div>
                     <p>
                         <btn class="btn-primary" @click="paymentMethodForm('manual')">{{ $t('wallet.add-credit') }}</btn>
                     </p>
                     <div id="mandate-form" v-if="!mandate.isValid && !mandate.isPending">
                         <template v-if="mandate.id && !mandate.isValid">
                             <div class="alert alert-danger">
                                 {{ $t('wallet.auto-payment-failed') }}
                             </div>
                             <btn class="btn-danger" @click="autoPaymentDelete">{{ $t('wallet.auto-payment-cancel') }}</btn>
                         </template>
                         <btn class="btn-primary" @click="paymentMethodForm('auto')">{{ $t('wallet.auto-payment-setup') }}</btn>
                     </div>
                     <div id="mandate-info" v-else>
                         <div v-if="mandate.isDisabled" class="disabled-mandate alert alert-danger">
                             {{ $t('wallet.auto-payment-disabled') }}
                         </div>
                         <template v-else>
                             <p v-html="$t('wallet.auto-payment-info', { amount: mandate.amount + ' ' + wallet.currency, balance: mandate.balance + ' ' + wallet.currency})"></p>
                             <p>{{ $t('wallet.payment-method', { method: mandate.method }) }}</p>
                         </template>
                         <div v-if="mandate.isPending" class="alert alert-warning">
                             {{ $t('wallet.auto-payment-inprogress') }}
                         </div>
                         <p class="buttons">
                             <btn class="btn-danger" @click="autoPaymentDelete">{{ $t('wallet.auto-payment-cancel') }}</btn>
                             <btn class="btn-primary" @click="autoPaymentChange">{{ $t('wallet.auto-payment-change') }}</btn>
                         </p>
                     </div>
                 </div>
             </div>
         </div>
 
-        <ul class="nav nav-tabs mt-3" role="tablist">
-            <li class="nav-item">
-                <a class="nav-link active" id="tab-receipts" href="#wallet-receipts" role="tab" aria-controls="wallet-receipts" aria-selected="true">
-                    {{ $t('wallet.receipts') }}
-                </a>
-            </li>
-            <li class="nav-item">
-                <a class="nav-link" id="tab-history" href="#wallet-history" role="tab" aria-controls="wallet-history" aria-selected="false">
-                    {{ $t('wallet.history') }}
-                </a>
-            </li>
-            <li v-if="showPendingPayments" class="nav-item">
-                <a class="nav-link" id="tab-payments" href="#wallet-payments" role="tab" aria-controls="wallet-payments" aria-selected="false">
-                    {{ $t('wallet.pending-payments') }}
-                </a>
-            </li>
-        </ul>
+        <tabs class="mt-3" ref="tabs" :tabs="tabs"></tabs>
+
         <div class="tab-content">
-            <div class="tab-pane active" id="wallet-receipts" role="tabpanel" aria-labelledby="tab-receipts">
+            <div class="tab-pane active" id="receipts" role="tabpanel" aria-labelledby="tab-receipts">
                 <div class="card-body">
                     <div class="card-text">
                         <p v-if="receipts.length">
                             {{ $t('wallet.receipts-hint') }}
                         </p>
                         <div v-if="receipts.length" class="input-group">
                             <select id="receipt-id" class="form-control">
                                 <option v-for="(receipt, index) in receipts" :key="index" :value="receipt">{{ receipt }}</option>
                             </select>
                             <btn class="btn-secondary" @click="receiptDownload" icon="download">{{ $t('btn.download') }}</btn>
                         </div>
                         <p v-if="!receipts.length">
                             {{ $t('wallet.receipts-none') }}
                         </p>
                     </div>
                 </div>
             </div>
-            <div class="tab-pane" id="wallet-history" role="tabpanel" aria-labelledby="tab-history">
+            <div class="tab-pane" id="history" role="tabpanel" aria-labelledby="tab-history">
                 <div class="card-body">
                     <transaction-log v-if="walletId && loadTransactions" class="card-text" :wallet-id="walletId"></transaction-log>
                 </div>
             </div>
-            <div class="tab-pane" id="wallet-payments" role="tabpanel" aria-labelledby="tab-payments">
+            <div class="tab-pane" id="payments" role="tabpanel" aria-labelledby="tab-payments">
                 <div class="card-body">
                     <payment-log v-if="walletId && loadPayments" class="card-text" :wallet-id="walletId"></payment-log>
                 </div>
             </div>
         </div>
 
         <modal-dialog id="payment-dialog" ref="paymentDialog" :title="paymentDialogTitle" @click="payment" :buttons="dialogButtons">
             <div id="payment-method" v-if="paymentForm == 'method'">
                 <form data-validation-prefix="mandate_">
                     <div id="payment-method-selection">
                         <a v-for="method in paymentMethods" :key="method.id" @click="selectPaymentMethod(method)" href="#" :class="'card link-' + method.id">
                             <svg-icon v-if="method.icon" :icon="[method.icon.prefix, method.icon.name]" />
                             <img v-if="method.image" :src="method.image" />
                             <span class="name">{{ method.name }}</span>
                         </a>
                     </div>
                 </form>
             </div>
             <div id="manual-payment" v-if="paymentForm == 'manual'">
                 <p v-if="wallet.currency != selectedPaymentMethod.currency">
                     {{ $t('wallet.currency-conv', { wc: wallet.currency, pc: selectedPaymentMethod.currency }) }}
                 </p>
                 <p v-if="selectedPaymentMethod.id == 'banktransfer'">
                     {{ $t('wallet.banktransfer-hint') }}
                 </p>
                 <p>
                     {{ $t('wallet.payment-amount-hint') }}
                 </p>
                 <form id="payment-form" @submit.prevent="payment">
                     <div class="input-group">
                         <input type="text" class="form-control" id="amount" v-model="amount" required>
                         <span class="input-group-text">{{ wallet.currency }}</span>
                     </div>
                     <div v-if="wallet.currency != selectedPaymentMethod.currency && !isNaN(amount)" class="alert alert-warning m-0 mt-3">
                         {{ $t('wallet.payment-warning', { price: $root.price(amount * selectedPaymentMethod.exchangeRate * 100, selectedPaymentMethod.currency) }) }}
                     </div>
                 </form>
             </div>
             <div id="auto-payment" v-if="paymentForm == 'auto'">
                 <form data-validation-prefix="mandate_">
                     <p>
                         {{ $t('wallet.auto-payment-hint') }}
                     </p>
                     <div class="row mb-3">
                         <label for="mandate_amount" class="col-sm-6 col-form-label">{{ $t('wallet.fill-up') }}</label>
                         <div class="col-sm-6">
                             <div class="input-group">
                                 <input type="text" class="form-control" id="mandate_amount" v-model="mandate.amount" required>
                                 <span class="input-group-text">{{ wallet.currency }}</span>
                             </div>
                         </div>
                     </div>
                     <div class="row mb-3">
                         <label for="mandate_balance" class="col-sm-6 col-form-label">{{ $t('wallet.when-below') }}</label>
                         <div class="col-sm-6">
                             <div class="input-group">
                                 <input type="text" class="form-control" id="mandate_balance" v-model="mandate.balance" required>
                                 <span class="input-group-text">{{ wallet.currency }}</span>
                             </div>
                         </div>
                     </div>
                     <p v-if="!mandate.isValid">
                         {{ $t('wallet.auto-payment-next') }}
                     </p>
                     <div v-if="mandate.isValid && mandate.isDisabled" class="disabled-mandate alert alert-danger m-0">
                         {{ $t('wallet.auto-payment-disabled-next') }}
                     </div>
                 </form>
             </div>
         </modal-dialog>
     </div>
 </template>
 
 <script>
     import ModalDialog from './Widgets/ModalDialog'
     import TransactionLog from './Widgets/TransactionLog'
     import PaymentLog from './Widgets/PaymentLog'
     import { downloadFile } from '../js/utils'
 
     import { library } from '@fortawesome/fontawesome-svg-core'
 
     library.add(
         require('@fortawesome/free-brands-svg-icons/faPaypal').definition,
         require('@fortawesome/free-regular-svg-icons/faCreditCard').definition,
         require('@fortawesome/free-solid-svg-icons/faBuildingColumns').definition,
     )
 
     export default {
         components: {
             ModalDialog,
             TransactionLog,
             PaymentLog
         },
         data() {
             return {
                 amount: '',
                 mandate: { amount: 10, balance: 0, method: null },
                 paymentDialogTitle: null,
                 paymentForm: null,
                 nextForm: null,
                 receipts: [],
                 stripe: null,
                 loadTransactions: false,
                 loadPayments: false,
                 showPendingPayments: false,
                 wallet: {},
                 walletId: null,
                 paymentMethods: [],
                 selectedPaymentMethod: null
             }
         },
         computed: {
             dialogButtons() {
                 if (this.paymentForm == 'method') {
                     return []
                 }
 
                 const button = {
                     className: 'btn-primary modal-action',
                     icon: 'check',
                     label: 'btn.submit'
                 }
 
                 if (this.paymentForm == 'manual'
                     || (this.paymentForm == 'auto' && !this.mandate.isValid && !this.mandate.isPending)
                 ) {
                     button.label = 'btn.continue'
                 }
 
                 return [ button ]
+            },
+            tabs() {
+                let tabs = [ 'wallet.receipts', 'wallet.history' ]
+                if (this.showPendingPayments) {
+                    tabs.push('wallet.pending-payments')
+                }
+                return tabs
             }
         },
         mounted() {
             $('#wallet button').focus()
 
             this.walletId = this.$root.authInfo.wallets[0].id
 
             axios.get('/api/v4/wallets/' + this.walletId, { loader: true })
                 .then(response => {
                     this.wallet = response.data
 
-                    axios.get('/api/v4/wallets/' + this.walletId + '/receipts', { loader: '#wallet-receipts' })
+                    axios.get('/api/v4/wallets/' + this.walletId + '/receipts', { loader: '#receipts' })
                         .then(response => {
                             this.receipts = response.data.list
                         })
 
                     if (this.wallet.provider == 'stripe') {
                         this.stripeInit()
                     }
                 })
                 .catch(this.$root.errorHandler)
 
             this.loadMandate()
 
             axios.get('/api/v4/payments/has-pending')
                 .then(response => {
                     this.showPendingPayments = response.data.hasPending
                 })
-        },
-        updated() {
-            $(this.$el).find('ul.nav-tabs a').on('click', e => {
-                this.$root.tab(e)
-
-                if ($(e.target).is('#tab-history')) {
-                    this.loadTransactions = true
-                } else if ($(e.target).is('#tab-payments')) {
-                    this.loadPayments = true
-                }
-            })
+
+            this.$refs.tabs.clickHandler('history', () => { this.loadTransactions = true })
+            this.$refs.tabs.clickHandler('payments', () => { this.loadPayments = true })
         },
         methods: {
             loadMandate() {
                 const loader = '#mandate-form'
 
                 this.$root.stopLoading(loader)
 
                 if (!this.mandate.id || this.mandate.isPending) {
                     axios.get('/api/v4/payments/mandate', { loader })
                         .then(response => {
                             this.mandate = response.data
                         })
                 }
             },
             selectPaymentMethod(method) {
                 this.formLock = false
                 this.selectedPaymentMethod = method
                 this.paymentForm = this.nextForm
 
                 setTimeout(() => { $('#payment-dialog').find('#amount,#mandate_amount').focus() }, 10)
             },
             payment() {
                 if (this.paymentForm == 'auto') {
                     return this.autoPayment()
                 }
 
                 if (this.formLock) {
                     return
                 }
 
                 // Lock the form to prevent from double submission
                 this.formLock = true
                 let onFinish = () => { this.formLock = false }
 
                 this.$root.clearFormValidation($('#payment-form'))
 
                 const post =  {
                     amount: this.amount,
                     methodId: this.selectedPaymentMethod.id,
                     currency: this.selectedPaymentMethod.currency
                 }
 
                 axios.post('/api/v4/payments', post, { onFinish })
                     .then(response => {
                         if (response.data.redirectUrl) {
                             location.href = response.data.redirectUrl
                         } else {
                             this.stripeCheckout(response.data)
                         }
                     })
             },
             autoPayment() {
                 if (this.formLock) {
                     return
                 }
 
                 // Lock the form to prevent from double submission
                 this.formLock = true
                 let onFinish = () => { this.formLock = false }
 
                 const method = this.mandate.id && (this.mandate.isValid || this.mandate.isPending) ? 'put' : 'post'
                 let post = {
                     amount: this.mandate.amount,
                     balance: this.mandate.balance,
                 }
 
                 // Modifications can't change the method of payment
                 if (this.selectedPaymentMethod) {
                     post.methodId = this.selectedPaymentMethod.id
                     post.currency = this.selectedPaymentMethod.currency
                 }
 
                 this.$root.clearFormValidation($('#auto-payment form'))
 
                 axios[method]('/api/v4/payments/mandate', post, { onFinish })
                     .then(response => {
                         if (method == 'post') {
                             this.mandate.id = null
                             // a new mandate, redirect to the chackout page
                             if (response.data.redirectUrl) {
                                 location.href = response.data.redirectUrl
                             } else if (response.data.id) {
                                 this.stripeCheckout(response.data)
                             }
                         } else {
                             // an update
                             if (response.data.status == 'success') {
                                 this.$refs.paymentDialog.hide();
                                 this.mandate = response.data
                                 this.$toast.success(response.data.message)
                             }
                         }
                     })
             },
             autoPaymentChange(event) {
                 this.autoPaymentForm(event, this.$t('wallet.auto-payment-update'))
             },
             autoPaymentDelete() {
                 axios.delete('/api/v4/payments/mandate')
                     .then(response => {
                         this.mandate = { amount: 10, balance: 0 }
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                         }
                     })
             },
             paymentMethodForm(nextForm) {
                 this.formLock = false
                 this.paymentMethods = []
                 this.paymentForm = 'method'
                 this.nextForm = nextForm
                 this.paymentDialogTitle = this.$t(nextForm == 'auto' ? 'wallet.auto-payment-setup' : 'wallet.top-up')
 
                 this.$refs.paymentDialog.show()
 
                 this.$nextTick().then(() => {
                     const type = nextForm == 'manual' ? 'oneoff' : 'recurring'
                     const loader = ['#payment-method', { 'min-height': '10em', small: false }]
 
                     axios.get('/api/v4/payments/methods', { params: { type }, loader })
                         .then(response => {
                             this.paymentMethods = response.data
                         })
                 })
             },
             autoPaymentForm(event, title) {
                 this.paymentForm = 'auto'
                 this.paymentDialogTitle = title
                 this.formLock = false
 
                 this.$refs.paymentDialog.show()
             },
             receiptDownload() {
                 const receipt = $('#receipt-id').val()
                 downloadFile('/api/v4/wallets/' + this.walletId + '/receipts/' + receipt)
             },
             stripeInit() {
                 let script = $('#stripe-script')
 
                 if (!script.length) {
                     script = document.createElement('script')
 
                     script.onload = () => {
                         this.stripe = Stripe(window.config.stripePK)
                     }
 
                     script.id = 'stripe-script'
                     script.src = 'https://js.stripe.com/v3/'
 
                     document.getElementsByTagName('head')[0].appendChild(script)
                 } else {
                     this.stripe = Stripe(window.config.stripePK)
                 }
             },
             stripeCheckout(data) {
                 if (!this.stripe) {
                     return
                 }
 
                 this.stripe.redirectToCheckout({
                     sessionId: data.id
                 }).then(result => {
                     // If it fails due to a browser or network error,
                     // display the localized error message to the user
                     if (result.error) {
                         this.$toast.error(result.error.message)
                     }
                 })
             }
         }
     }
 </script>
diff --git a/src/resources/vue/Widgets/Tabs.vue b/src/resources/vue/Widgets/Tabs.vue
new file mode 100644
index 00000000..5ca7fd3a
--- /dev/null
+++ b/src/resources/vue/Widgets/Tabs.vue
@@ -0,0 +1,65 @@
+<template>
+    <ul class="nav nav-tabs" role="tablist">
+        <li v-for="(tab, index) in tabs" :key="index" class="nav-item">
+            <a role="tab" :aria-controls="tabKey(tab)" :aria-selected="!index" @click="tabClick"
+               :class="'nav-link' + (!index ? ' active' : '')"
+               :id="'tab-' + tabKey(tab)"
+               :href="'#' + tabKey(tab)"
+            >
+                {{ $t(tabLabel(tab)) + (typeof tab != 'string' && 'count' in tab ? ` (${tab.count})` : '') }}
+            </a>
+        </li>
+    </ul>
+</template>
+
+<script>
+    import { Tab } from 'bootstrap'
+
+    export default {
+        props: {
+            tabs: { type: Array, default: () => [] }
+        },
+        data() {
+            return {
+                clickHandlers: {}
+            }
+        },
+        methods: {
+            tabClick(event) {
+                event.preventDefault()
+
+                new Tab(event.target).show()
+
+                const key = event.target.id.replace('tab-', '')
+
+                if (key in this.clickHandlers) {
+                    this.clickHandlers[key](event)
+                }
+            },
+            tabKey(tab) {
+                return this.tabLabel(tab).split(/[.-]/).slice(-1)
+            },
+            tabIndex(key) {
+                return this.tabs.findIndex(tab => this.tabKey(tab) == key)
+            },
+            tabLabel(tab) {
+                return typeof tab == 'string' ? tab : tab.label
+            },
+            updateCounter(key, count) {
+                const index = this.tabIndex(key)
+                let tab = this.tabs[index]
+
+                if (typeof tab == 'string') {
+                    tab = { label: tab, count }
+                } else {
+                    tab.count = count
+                }
+
+                this.$set(this.tabs, index, tab)
+            },
+            clickHandler(key, callback) {
+                this.clickHandlers[key] = callback
+            }
+        }
+    }
+</script>
diff --git a/src/tests/Browser/Admin/SharedFolderTest.php b/src/tests/Browser/Admin/SharedFolderTest.php
index e836c482..7a8ea651 100644
--- a/src/tests/Browser/Admin/SharedFolderTest.php
+++ b/src/tests/Browser/Admin/SharedFolderTest.php
@@ -1,109 +1,109 @@
 <?php
 
 namespace Tests\Browser\Admin;
 
 use App\SharedFolder;
 use Illuminate\Support\Facades\Queue;
 use Tests\Browser;
 use Tests\Browser\Components\Toast;
 use Tests\Browser\Pages\Admin\SharedFolder as SharedFolderPage;
 use Tests\Browser\Pages\Admin\User as UserPage;
 use Tests\Browser\Pages\Dashboard;
 use Tests\Browser\Pages\Home;
 use Tests\TestCaseDusk;
 
 class SharedFolderTest extends TestCaseDusk
 {
     /**
      * {@inheritDoc}
      */
     public function setUp(): void
     {
         parent::setUp();
         self::useAdminUrl();
     }
 
     /**
      * {@inheritDoc}
      */
     public function tearDown(): void
     {
         parent::tearDown();
     }
 
     /**
      * Test shared folder info page (unauthenticated)
      */
     public function testSharedFolderUnauth(): void
     {
         // Test that the page requires authentication
         $this->browse(function (Browser $browser) {
             $user = $this->getTestUser('john@kolab.org');
             $folder = $this->getTestSharedFolder('folder-event@kolab.org');
 
             $browser->visit('/shared-folder/' . $folder->id)->on(new Home());
         });
     }
 
     /**
      * Test shared folder info page
      */
     public function testInfo(): void
     {
         Queue::fake();
 
         $this->browse(function (Browser $browser) {
             $user = $this->getTestUser('john@kolab.org');
             $folder = $this->getTestSharedFolder('folder-event@kolab.org');
             $folder->setConfig(['acl' => ['anyone, read-only', 'jack@kolab.org, read-write']]);
             $folder->setAliases(['folder-alias1@kolab.org', 'folder-alias2@kolab.org']);
             $folder->status = SharedFolder::STATUS_NEW | SharedFolder::STATUS_ACTIVE
                 | SharedFolder::STATUS_LDAP_READY | SharedFolder::STATUS_IMAP_READY;
             $folder->save();
 
             $folder_page = new SharedFolderPage($folder->id);
             $user_page = new UserPage($user->id);
 
             // Goto the folder page
             $browser->visit(new Home())
                 ->submitLogon('jeroen@jeroen.jeroen', \App\Utils::generatePassphrase(), true)
                 ->on(new Dashboard())
                 ->visit($user_page)
                 ->on($user_page)
-                ->click('@nav #tab-shared-folders')
+                ->click('@nav #tab-folders')
                 ->pause(1000)
-                ->click('@user-shared-folders table tbody tr:first-child td:first-child a')
+                ->click('@user-folders table tbody tr:first-child td:first-child a')
                 ->on($folder_page)
                 ->assertSeeIn('@folder-info .card-title', $folder->email)
                 ->with('@folder-info form', function (Browser $browser) use ($folder) {
                     $browser->assertElementsCount('.row', 4)
                         ->assertSeeIn('.row:nth-child(1) label', 'ID (Created)')
                         ->assertSeeIn('.row:nth-child(1) #folderid', "{$folder->id} ({$folder->created_at})")
                         ->assertSeeIn('.row:nth-child(2) label', 'Status')
                         ->assertSeeIn('.row:nth-child(2) #status.text-success', 'Active')
                         ->assertSeeIn('.row:nth-child(3) label', 'Name')
                         ->assertSeeIn('.row:nth-child(3) #name', $folder->name)
                         ->assertSeeIn('.row:nth-child(4) label', 'Type')
                         ->assertSeeIn('.row:nth-child(4) #type', 'Calendar');
                 })
                 ->assertElementsCount('ul.nav-tabs .nav-item', 2)
                 ->assertSeeIn('ul.nav-tabs .nav-item:nth-child(1) .nav-link', 'Settings')
                 ->with('@folder-settings form', function (Browser $browser) {
                     $browser->assertElementsCount('.row', 1)
                         ->assertSeeIn('.row:nth-child(1) label', 'Access rights')
                         ->assertSeeIn('.row:nth-child(1) #acl', 'anyone: read-only')
                         ->assertSeeIn('.row:nth-child(1) #acl', 'jack@kolab.org: read-write');
                 })
                 ->assertSeeIn('ul.nav-tabs .nav-item:nth-child(2) .nav-link', 'Email Aliases (2)')
                 ->click('ul.nav-tabs .nav-item:nth-child(2) .nav-link')
                 ->with('@folder-aliases table', function (Browser $browser) {
                     $browser->assertElementsCount('tbody tr', 2)
                         ->assertSeeIn('tbody tr:nth-child(1) td', 'folder-alias1@kolab.org')
                         ->assertSeeIn('tbody tr:nth-child(2) td', 'folder-alias2@kolab.org');
                 });
 
             // Test invalid shared folder identifier
             $browser->visit('/shared-folder/abc')->assertErrorPage(404);
         });
     }
 }
diff --git a/src/tests/Browser/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php
index 310b2a4c..9bc0aa0b 100644
--- a/src/tests/Browser/Admin/UserTest.php
+++ b/src/tests/Browser/Admin/UserTest.php
@@ -1,604 +1,604 @@
 <?php
 
 namespace Tests\Browser\Admin;
 
 use App\Auth\SecondFactor;
 use App\Discount;
 use App\Entitlement;
 use App\Sku;
 use App\User;
 use Tests\Browser;
 use Tests\Browser\Components\Dialog;
 use Tests\Browser\Components\Toast;
 use Tests\Browser\Pages\Admin\User as UserPage;
 use Tests\Browser\Pages\Dashboard;
 use Tests\Browser\Pages\Home;
 use Tests\TestCaseDusk;
 use Illuminate\Foundation\Testing\DatabaseMigrations;
 
 class UserTest extends TestCaseDusk
 {
     /**
      * {@inheritDoc}
      */
     public function setUp(): void
     {
         parent::setUp();
         self::useAdminUrl();
 
         $john = $this->getTestUser('john@kolab.org');
         $john->setSettings([
                 'phone' => '+48123123123',
                 'external_email' => 'john.doe.external@gmail.com',
         ]);
         if ($john->isSuspended()) {
             User::where('email', $john->email)->update(['status' => $john->status - User::STATUS_SUSPENDED]);
         }
         $wallet = $john->wallets()->first();
         $wallet->discount()->dissociate();
         $wallet->save();
 
         Entitlement::where('cost', '>=', 5000)->delete();
         $this->deleteTestGroup('group-test@kolab.org');
         $this->clearMeetEntitlements();
     }
 
     /**
      * {@inheritDoc}
      */
     public function tearDown(): void
     {
         $john = $this->getTestUser('john@kolab.org');
         $john->setSettings([
                 'phone' => null,
                 'external_email' => 'john.doe.external@gmail.com',
         ]);
         if ($john->isSuspended()) {
             User::where('email', $john->email)->update(['status' => $john->status - User::STATUS_SUSPENDED]);
         }
         $wallet = $john->wallets()->first();
         $wallet->discount()->dissociate();
         $wallet->save();
 
         Entitlement::where('cost', '>=', 5000)->delete();
         $this->deleteTestGroup('group-test@kolab.org');
         $this->clearMeetEntitlements();
 
         parent::tearDown();
     }
 
     /**
      * Test user info page (unauthenticated)
      */
     public function testUserUnauth(): void
     {
         // Test that the page requires authentication
         $this->browse(function (Browser $browser) {
             $jack = $this->getTestUser('jack@kolab.org');
             $browser->visit('/user/' . $jack->id)->on(new Home());
         });
     }
 
     /**
      * Test user info page
      */
     public function testUserInfo(): void
     {
         $this->browse(function (Browser $browser) {
             $jack = $this->getTestUser('jack@kolab.org');
             $page = new UserPage($jack->id);
 
             $browser->visit(new Home())
                 ->submitLogon('jeroen@jeroen.jeroen', \App\Utils::generatePassphrase(), true)
                 ->on(new Dashboard())
                 ->visit($page)
                 ->on($page);
 
             // Assert main info box content
             $browser->assertSeeIn('@user-info .card-title', $jack->email)
                 ->with('@user-info form', function (Browser $browser) use ($jack) {
                     $browser->assertElementsCount('.row', 7)
                         ->assertSeeIn('.row:nth-child(1) label', 'Managed by')
                         ->assertSeeIn('.row:nth-child(1) #manager a', 'john@kolab.org')
                         ->assertSeeIn('.row:nth-child(2) label', 'ID (Created)')
                         ->assertSeeIn('.row:nth-child(2) #userid', "{$jack->id} ({$jack->created_at})")
                         ->assertSeeIn('.row:nth-child(3) label', 'Status')
                         ->assertSeeIn('.row:nth-child(3) #status span.text-success', 'Active')
                         ->assertSeeIn('.row:nth-child(4) label', 'First Name')
                         ->assertSeeIn('.row:nth-child(4) #first_name', 'Jack')
                         ->assertSeeIn('.row:nth-child(5) label', 'Last Name')
                         ->assertSeeIn('.row:nth-child(5) #last_name', 'Daniels')
                         ->assertSeeIn('.row:nth-child(6) label', 'External Email')
                         ->assertMissing('.row:nth-child(6) #external_email a')
                         ->assertSeeIn('.row:nth-child(7) label', 'Country')
                         ->assertSeeIn('.row:nth-child(7) #country', 'United States');
                 });
 
             // Some tabs are loaded in background, wait a second
             $browser->pause(500)
                 ->assertElementsCount('@nav a', 9);
 
             // Note: Finances tab is tested in UserFinancesTest.php
             $browser->assertSeeIn('@nav #tab-finances', 'Finances');
 
             // Assert Aliases tab
             $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (1)')
                 ->click('@nav #tab-aliases')
                 ->whenAvailable('@user-aliases', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 1)
                         ->assertSeeIn('table tbody tr:first-child td:first-child', 'jack.daniels@kolab.org')
                         ->assertMissing('table tfoot');
                 });
 
             // Assert Subscriptions tab
             $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (3)')
                 ->click('@nav #tab-subscriptions')
                 ->with('@user-subscriptions', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 3)
                         ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox')
                         ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '5,00 CHF')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 5 GB')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF')
                         ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
                         ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,90 CHF')
                         ->assertMissing('table tfoot')
                         ->assertMissing('#reset2fa');
                 });
 
             // Assert Domains tab
             $browser->assertSeeIn('@nav #tab-domains', 'Domains (0)')
                 ->click('@nav #tab-domains')
                 ->with('@user-domains', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no domains in this account.');
                 });
 
             // Assert Users tab
             $browser->assertSeeIn('@nav #tab-users', 'Users (0)')
                 ->click('@nav #tab-users')
                 ->with('@user-users', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no users in this account.');
                 });
 
             // Assert Distribution lists tab
             $browser->assertSeeIn('@nav #tab-distlists', 'Distribution lists (0)')
                 ->click('@nav #tab-distlists')
                 ->with('@user-distlists', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no distribution lists in this account.');
                 });
 
             // Assert Resources tab
             $browser->assertSeeIn('@nav #tab-resources', 'Resources (0)')
                 ->click('@nav #tab-resources')
                 ->with('@user-resources', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no resources in this account.');
                 });
 
             // Assert Shared folders tab
-            $browser->assertSeeIn('@nav #tab-shared-folders', 'Shared folders (0)')
-                ->click('@nav #tab-shared-folders')
-                ->with('@user-shared-folders', function (Browser $browser) {
+            $browser->assertSeeIn('@nav #tab-folders', 'Shared folders (0)')
+                ->click('@nav #tab-folders')
+                ->with('@user-folders', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no shared folders in this account.');
                 });
 
             // Assert Settings tab
             $browser->assertSeeIn('@nav #tab-settings', 'Settings')
                 ->click('@nav #tab-settings')
                 ->whenAvailable('@user-settings form', function (Browser $browser) {
                     $browser->assertElementsCount('.row', 1)
                         ->assertSeeIn('.row:first-child label', 'Greylisting')
                         ->assertSeeIn('.row:first-child .text-success', 'enabled');
                 });
         });
     }
 
     /**
      * Test user info page (continue)
      *
      * @depends testUserInfo
      */
     public function testUserInfo2(): void
     {
         $this->browse(function (Browser $browser) {
             $john = $this->getTestUser('john@kolab.org');
             $page = new UserPage($john->id);
             $discount = Discount::where('code', 'TEST')->first();
             $wallet = $john->wallet();
             $wallet->discount()->associate($discount);
             $wallet->debit(2010);
             $wallet->save();
             $group = $this->getTestGroup('group-test@kolab.org', ['name' => 'Test Group']);
             $group->assignToWallet($john->wallets->first());
             $john->setSetting('greylist_enabled', null);
 
             // Click the managed-by link on Jack's page
             $browser->click('@user-info #manager a')
                 ->on($page);
 
             // Assert main info box content
             $browser->assertSeeIn('@user-info .card-title', $john->email)
                 ->with('@user-info form', function (Browser $browser) use ($john) {
                     $ext_email = $john->getSetting('external_email');
 
                     $browser->assertElementsCount('.row', 9)
                         ->assertSeeIn('.row:nth-child(1) label', 'ID (Created)')
                         ->assertSeeIn('.row:nth-child(1) #userid', "{$john->id} ({$john->created_at})")
                         ->assertSeeIn('.row:nth-child(2) label', 'Status')
                         ->assertSeeIn('.row:nth-child(2) #status span.text-success', 'Active')
                         ->assertSeeIn('.row:nth-child(3) label', 'First Name')
                         ->assertSeeIn('.row:nth-child(3) #first_name', 'John')
                         ->assertSeeIn('.row:nth-child(4) label', 'Last Name')
                         ->assertSeeIn('.row:nth-child(4) #last_name', 'Doe')
                         ->assertSeeIn('.row:nth-child(5) label', 'Organization')
                         ->assertSeeIn('.row:nth-child(5) #organization', 'Kolab Developers')
                         ->assertSeeIn('.row:nth-child(6) label', 'Phone')
                         ->assertSeeIn('.row:nth-child(6) #phone', $john->getSetting('phone'))
                         ->assertSeeIn('.row:nth-child(7) label', 'External Email')
                         ->assertSeeIn('.row:nth-child(7) #external_email a', $ext_email)
                         ->assertAttribute('.row:nth-child(7) #external_email a', 'href', "mailto:$ext_email")
                         ->assertSeeIn('.row:nth-child(8) label', 'Address')
                         ->assertSeeIn('.row:nth-child(8) #billing_address', $john->getSetting('billing_address'))
                         ->assertSeeIn('.row:nth-child(9) label', 'Country')
                         ->assertSeeIn('.row:nth-child(9) #country', 'United States');
                 });
 
             // Some tabs are loaded in background, wait a second
             $browser->pause(500)
                 ->assertElementsCount('@nav a', 9);
 
             // Note: Finances tab is tested in UserFinancesTest.php
             $browser->assertSeeIn('@nav #tab-finances', 'Finances');
 
             // Assert Aliases tab
             $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (1)')
                 ->click('@nav #tab-aliases')
                 ->whenAvailable('@user-aliases', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 1)
                         ->assertSeeIn('table tbody tr:first-child td:first-child', 'john.doe@kolab.org')
                         ->assertMissing('table tfoot');
                 });
 
             // Assert Subscriptions tab
             $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (3)')
                 ->click('@nav #tab-subscriptions')
                 ->with('@user-subscriptions', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 3)
                         ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox')
                         ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,50 CHF/month¹')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 5 GB')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹')
                         ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
                         ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,41 CHF/month¹')
                         ->assertMissing('table tfoot')
                         ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher');
                 });
 
             // Assert Domains tab
             $browser->assertSeeIn('@nav #tab-domains', 'Domains (1)')
                 ->click('@nav #tab-domains')
                 ->with('@user-domains table', function (Browser $browser) {
                     $browser->assertElementsCount('tbody tr', 1)
                         ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'kolab.org')
                         ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-success')
                         ->assertMissing('tfoot');
                 });
 
             // Assert Users tab
             $browser->assertSeeIn('@nav #tab-users', 'Users (4)')
                 ->click('@nav #tab-users')
                 ->with('@user-users table', function (Browser $browser) {
                     $browser->assertElementsCount('tbody tr', 4)
                         ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'jack@kolab.org')
                         ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-success')
                         ->assertSeeIn('tbody tr:nth-child(2) td:first-child a', 'joe@kolab.org')
                         ->assertVisible('tbody tr:nth-child(2) td:first-child svg.text-success')
                         ->assertSeeIn('tbody tr:nth-child(3) td:first-child span', 'john@kolab.org')
                         ->assertVisible('tbody tr:nth-child(3) td:first-child svg.text-success')
                         ->assertSeeIn('tbody tr:nth-child(4) td:first-child a', 'ned@kolab.org')
                         ->assertVisible('tbody tr:nth-child(4) td:first-child svg.text-success')
                         ->assertMissing('tfoot');
                 });
 
             // Assert Distribution lists tab
             $browser->assertSeeIn('@nav #tab-distlists', 'Distribution lists (1)')
                 ->click('@nav #tab-distlists')
                 ->with('@user-distlists table', function (Browser $browser) {
                     $browser->assertElementsCount('tbody tr', 1)
                         ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'Test Group')
                         ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-danger')
                         ->assertSeeIn('tbody tr:nth-child(1) td:last-child a', 'group-test@kolab.org')
                         ->assertMissing('tfoot');
                 });
 
             // Assert Resources tab
             $browser->assertSeeIn('@nav #tab-resources', 'Resources (2)')
                 ->click('@nav #tab-resources')
                 ->with('@user-resources', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 2)
                         ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'Conference Room #1')
                         ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', 'resource-test1@kolab.org')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Conference Room #2')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', 'resource-test2@kolab.org')
                         ->assertMissing('table tfoot');
                 });
 
             // Assert Shared folders tab
-            $browser->assertSeeIn('@nav #tab-shared-folders', 'Shared folders (2)')
-                ->click('@nav #tab-shared-folders')
-                ->with('@user-shared-folders', function (Browser $browser) {
+            $browser->assertSeeIn('@nav #tab-folders', 'Shared folders (2)')
+                ->click('@nav #tab-folders')
+                ->with('@user-folders', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 2)
                         ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'Calendar')
                         ->assertSeeIn('table tbody tr:nth-child(1) td:nth-child(2)', 'Calendar')
                         ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', 'folder-event@kolab.org')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Contacts')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:nth-child(2)', 'Address Book')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', 'folder-contact@kolab.org')
                         ->assertMissing('table tfoot');
                 });
         });
 
         // Now we go to Ned's info page, he's a controller on John's wallet
         $this->browse(function (Browser $browser) {
             $ned = $this->getTestUser('ned@kolab.org');
             $beta_sku = Sku::withEnvTenantContext()->where('title', 'beta')->first();
             $storage_sku = Sku::withEnvTenantContext()->where('title', 'storage')->first();
             $wallet = $ned->wallet();
 
             // Add an extra storage and beta entitlement with different prices
             Entitlement::create([
                     'wallet_id' => $wallet->id,
                     'sku_id' => $beta_sku->id,
                     'cost' => 5010,
                     'entitleable_id' => $ned->id,
                     'entitleable_type' => User::class
             ]);
             Entitlement::create([
                     'wallet_id' => $wallet->id,
                     'sku_id' => $storage_sku->id,
                     'cost' => 5000,
                     'entitleable_id' => $ned->id,
                     'entitleable_type' => User::class
             ]);
 
             $page = new UserPage($ned->id);
             $ned->setSetting('greylist_enabled', 'false');
 
             $browser->click('@nav #tab-users')
                 ->click('@user-users tbody tr:nth-child(4) td:first-child a')
                 ->on($page);
 
             // Assert main info box content
             $browser->assertSeeIn('@user-info .card-title', $ned->email)
                 ->with('@user-info form', function (Browser $browser) use ($ned) {
                     $browser->assertSeeIn('.row:nth-child(2) label', 'ID (Created)')
                         ->assertSeeIn('.row:nth-child(2) #userid', "{$ned->id} ({$ned->created_at})");
                 });
 
             // Some tabs are loaded in background, wait a second
             $browser->pause(500)
                 ->assertElementsCount('@nav a', 9);
 
             // Note: Finances tab is tested in UserFinancesTest.php
             $browser->assertSeeIn('@nav #tab-finances', 'Finances');
 
             // Assert Aliases tab
             $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (0)')
                 ->click('@nav #tab-aliases')
                 ->whenAvailable('@user-aliases', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'This user has no email aliases.');
                 });
 
             // Assert Subscriptions tab, we expect John's discount here
             $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (6)')
                 ->click('@nav #tab-subscriptions')
                 ->with('@user-subscriptions', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 6)
                         ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox')
                         ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,50 CHF/month¹')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 6 GB')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '45,00 CHF/month¹')
                         ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
                         ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,41 CHF/month¹')
                         ->assertSeeIn('table tbody tr:nth-child(4) td:first-child', 'Activesync')
                         ->assertSeeIn('table tbody tr:nth-child(4) td:last-child', '0,00 CHF/month¹')
                         ->assertSeeIn('table tbody tr:nth-child(5) td:first-child', '2-Factor Authentication')
                         ->assertSeeIn('table tbody tr:nth-child(5) td:last-child', '0,00 CHF/month¹')
                         ->assertSeeIn('table tbody tr:nth-child(6) td:first-child', 'Private Beta (invitation only)')
                         ->assertSeeIn('table tbody tr:nth-child(6) td:last-child', '45,09 CHF/month¹')
                         ->assertMissing('table tfoot')
                         ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher')
                         ->assertSeeIn('#reset2fa', 'Reset 2-Factor Auth')
                         ->assertMissing('#addbetasku');
                 });
 
             // We don't expect John's domains here
             $browser->assertSeeIn('@nav #tab-domains', 'Domains (0)')
                 ->click('@nav #tab-domains')
                 ->with('@user-domains', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no domains in this account.');
                 });
 
             // We don't expect John's users here
             $browser->assertSeeIn('@nav #tab-users', 'Users (0)')
                 ->click('@nav #tab-users')
                 ->with('@user-users', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no users in this account.');
                 });
 
             // We don't expect John's distribution lists here
             $browser->assertSeeIn('@nav #tab-distlists', 'Distribution lists (0)')
                 ->click('@nav #tab-distlists')
                 ->with('@user-distlists', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no distribution lists in this account.');
                 });
 
             // We don't expect John's resources here
             $browser->assertSeeIn('@nav #tab-resources', 'Resources (0)')
                 ->click('@nav #tab-resources')
                 ->with('@user-resources', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no resources in this account.');
                 });
 
             // We don't expect John's folders here
-            $browser->assertSeeIn('@nav #tab-shared-folders', 'Shared folders (0)')
-                ->click('@nav #tab-shared-folders')
-                ->with('@user-shared-folders', function (Browser $browser) {
+            $browser->assertSeeIn('@nav #tab-folders', 'Shared folders (0)')
+                ->click('@nav #tab-folders')
+                ->with('@user-folders', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no shared folders in this account.');
                 });
 
             // Assert Settings tab
             $browser->assertSeeIn('@nav #tab-settings', 'Settings')
                 ->click('@nav #tab-settings')
                 ->whenAvailable('@user-settings form', function (Browser $browser) {
                     $browser->assertElementsCount('.row', 1)
                         ->assertSeeIn('.row:first-child label', 'Greylisting')
                         ->assertSeeIn('.row:first-child .text-danger', 'disabled');
                 });
         });
     }
 
     /**
      * Test editing an external email
      *
      * @depends testUserInfo2
      */
     public function testExternalEmail(): void
     {
         $this->browse(function (Browser $browser) {
             $john = $this->getTestUser('john@kolab.org');
 
             $browser->visit(new UserPage($john->id))
                 ->waitFor('@user-info #external_email button')
                 ->click('@user-info #external_email button')
                 // Test dialog content, and closing it with Cancel button
                 ->with(new Dialog('#email-dialog'), function (Browser $browser) {
                     $browser->assertSeeIn('@title', 'External Email')
                         ->assertFocused('@body input')
                         ->assertValue('@body input', 'john.doe.external@gmail.com')
                         ->assertSeeIn('@button-cancel', 'Cancel')
                         ->assertSeeIn('@button-action', 'Submit')
                         ->click('@button-cancel');
                 })
                 ->assertMissing('#email-dialog')
                 ->click('@user-info #external_email button')
                 // Test email validation error handling, and email update
                 ->with(new Dialog('#email-dialog'), function (Browser $browser) {
                     $browser->type('@body input', 'test')
                         ->click('@button-action')
                         ->waitFor('@body input.is-invalid')
                         ->assertSeeIn(
                             '@body input + .invalid-feedback',
                             'The external email must be a valid email address.'
                         )
                         ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
                         ->type('@body input', 'test@test.com')
                         ->click('@button-action');
                 })
                 ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.')
                 ->assertSeeIn('@user-info #external_email a', 'test@test.com')
                 ->click('@user-info #external_email button')
                 ->with(new Dialog('#email-dialog'), function (Browser $browser) {
                     $browser->assertValue('@body input', 'test@test.com')
                         ->assertMissing('@body input.is-invalid')
                         ->assertMissing('@body input + .invalid-feedback')
                         ->click('@button-cancel');
                 })
                 ->assertSeeIn('@user-info #external_email a', 'test@test.com');
 
             // $john->getSetting() may not work here as it uses internal cache
             // read the value form database
             $current_ext_email = $john->settings()->where('key', 'external_email')->first()->value;
             $this->assertSame('test@test.com', $current_ext_email);
         });
     }
 
     /**
      * Test suspending/unsuspending the user
      */
     public function testSuspendAndUnsuspend(): void
     {
         $this->browse(function (Browser $browser) {
             $john = $this->getTestUser('john@kolab.org');
 
             $browser->visit(new UserPage($john->id))
                 ->assertVisible('@user-info #button-suspend')
                 ->assertMissing('@user-info #button-unsuspend')
                 ->click('@user-info #button-suspend')
                 ->assertToast(Toast::TYPE_SUCCESS, 'User suspended successfully.')
                 ->assertSeeIn('@user-info #status span.text-warning', 'Suspended')
                 ->assertMissing('@user-info #button-suspend')
                 ->click('@user-info #button-unsuspend')
                 ->assertToast(Toast::TYPE_SUCCESS, 'User unsuspended successfully.')
                 ->assertSeeIn('@user-info #status span.text-success', 'Active')
                 ->assertVisible('@user-info #button-suspend')
                 ->assertMissing('@user-info #button-unsuspend');
         });
     }
 
     /**
      * Test resetting 2FA for the user
      */
     public function testReset2FA(): void
     {
         $this->browse(function (Browser $browser) {
             $this->deleteTestUser('userstest1@kolabnow.com');
             $user = $this->getTestUser('userstest1@kolabnow.com');
             $sku2fa = Sku::withEnvTenantContext()->where('title', '2fa')->first();
             $user->assignSku($sku2fa);
             SecondFactor::seed('userstest1@kolabnow.com');
 
             $browser->visit(new UserPage($user->id))
                 ->click('@nav #tab-subscriptions')
                 ->with('@user-subscriptions', function (Browser $browser) use ($sku2fa) {
                     $browser->waitFor('#reset2fa')
                         ->assertVisible('#sku' . $sku2fa->id);
                 })
                 ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (1)')
                 ->click('#reset2fa')
                 ->with(new Dialog('#reset-2fa-dialog'), function (Browser $browser) {
                     $browser->assertSeeIn('@title', '2-Factor Authentication Reset')
                         ->assertSeeIn('@button-cancel', 'Cancel')
                         ->assertSeeIn('@button-action', 'Reset')
                         ->click('@button-action');
                 })
                 ->assertToast(Toast::TYPE_SUCCESS, '2-Factor authentication reset successfully.')
                 ->assertMissing('#sku' . $sku2fa->id)
                 ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (0)');
         });
     }
 
     /**
      * Test adding the beta SKU for the user
      */
     public function testAddBetaSku(): void
     {
         $this->browse(function (Browser $browser) {
             $this->deleteTestUser('userstest1@kolabnow.com');
             $user = $this->getTestUser('userstest1@kolabnow.com');
             $sku = Sku::withEnvTenantContext()->where('title', 'beta')->first();
 
             $browser->visit(new UserPage($user->id))
                 ->click('@nav #tab-subscriptions')
                 ->waitFor('@user-subscriptions #addbetasku')
                 ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (0)')
                 ->assertSeeIn('#addbetasku', 'Enable beta program')
                 ->click('#addbetasku')
                 ->assertToast(Toast::TYPE_SUCCESS, 'The subscription added successfully.')
                 ->waitFor('#sku' . $sku->id)
                 ->assertSeeIn("#sku{$sku->id} td:first-child", 'Private Beta (invitation only)')
                 ->assertSeeIn("#sku{$sku->id} td:last-child", '0,00 CHF/month')
                 ->assertMissing('#addbetasku')
                 ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (1)');
         });
     }
 }
diff --git a/src/tests/Browser/Pages/Admin/Distlist.php b/src/tests/Browser/Pages/Admin/Distlist.php
index e1edd794..ecec0484 100644
--- a/src/tests/Browser/Pages/Admin/Distlist.php
+++ b/src/tests/Browser/Pages/Admin/Distlist.php
@@ -1,57 +1,57 @@
 <?php
 
 namespace Tests\Browser\Pages\Admin;
 
 use Laravel\Dusk\Page;
 
 class Distlist extends Page
 {
     protected $listid;
 
     /**
      * Object constructor.
      *
      * @param int $listid Distribution list Id
      */
     public function __construct($listid)
     {
         $this->listid = $listid;
     }
 
     /**
      * Get the URL for the page.
      *
      * @return string
      */
     public function url(): string
     {
         return '/distlist/' . $this->listid;
     }
 
     /**
      * Assert that the browser is on the page.
      *
      * @param \Laravel\Dusk\Browser $browser The browser object
      *
      * @return void
      */
     public function assert($browser): void
     {
         $browser->waitForLocation($this->url())
             ->waitFor('@distlist-info');
     }
 
     /**
      * Get the element shortcuts for the page.
      *
      * @return array
      */
     public function elements(): array
     {
         return [
             '@app' => '#app',
             '@distlist-info' => '#distlist-info',
-            '@distlist-settings' => '#distlist-settings',
+            '@distlist-settings' => '#settings',
         ];
     }
 }
diff --git a/src/tests/Browser/Pages/Admin/Domain.php b/src/tests/Browser/Pages/Admin/Domain.php
index a7f9a20a..c8d81ee7 100644
--- a/src/tests/Browser/Pages/Admin/Domain.php
+++ b/src/tests/Browser/Pages/Admin/Domain.php
@@ -1,59 +1,59 @@
 <?php
 
 namespace Tests\Browser\Pages\Admin;
 
 use Laravel\Dusk\Page;
 
 class Domain extends Page
 {
     protected $domainid;
 
     /**
      * Object constructor.
      *
      * @param int $domainid Domain Id
      */
     public function __construct($domainid)
     {
         $this->domainid = $domainid;
     }
 
     /**
      * Get the URL for the page.
      *
      * @return string
      */
     public function url(): string
     {
         return '/domain/' . $this->domainid;
     }
 
     /**
      * Assert that the browser is on the page.
      *
      * @param \Laravel\Dusk\Browser $browser The browser object
      *
      * @return void
      */
     public function assert($browser): void
     {
         $browser->waitForLocation($this->url())
             ->waitFor('@domain-info');
     }
 
     /**
      * Get the element shortcuts for the page.
      *
      * @return array
      */
     public function elements(): array
     {
         return [
             '@app' => '#app',
             '@domain-info' => '#domain-info',
             '@nav' => 'ul.nav-tabs',
-            '@domain-config' => '#domain-config',
-            '@domain-settings' => '#domain-settings',
+            '@domain-config' => '#config',
+            '@domain-settings' => '#settings',
         ];
     }
 }
diff --git a/src/tests/Browser/Pages/Admin/Resource.php b/src/tests/Browser/Pages/Admin/Resource.php
index cfe6269f..4123ac11 100644
--- a/src/tests/Browser/Pages/Admin/Resource.php
+++ b/src/tests/Browser/Pages/Admin/Resource.php
@@ -1,57 +1,57 @@
 <?php
 
 namespace Tests\Browser\Pages\Admin;
 
 use Laravel\Dusk\Page;
 
 class Resource extends Page
 {
     protected $resourceId;
 
     /**
      * Object constructor.
      *
      * @param int $id Resource Id
      */
     public function __construct($id)
     {
         $this->resourceId = $id;
     }
 
     /**
      * Get the URL for the page.
      *
      * @return string
      */
     public function url(): string
     {
         return '/resource/' . $this->resourceId;
     }
 
     /**
      * Assert that the browser is on the page.
      *
      * @param \Laravel\Dusk\Browser $browser The browser object
      *
      * @return void
      */
     public function assert($browser): void
     {
         $browser->waitForLocation($this->url())
             ->waitFor('@resource-info');
     }
 
     /**
      * Get the element shortcuts for the page.
      *
      * @return array
      */
     public function elements(): array
     {
         return [
             '@app' => '#app',
             '@resource-info' => '#resource-info',
-            '@resource-settings' => '#resource-settings',
+            '@resource-settings' => '#settings',
         ];
     }
 }
diff --git a/src/tests/Browser/Pages/Admin/SharedFolder.php b/src/tests/Browser/Pages/Admin/SharedFolder.php
index 9aed148a..3080f8f7 100644
--- a/src/tests/Browser/Pages/Admin/SharedFolder.php
+++ b/src/tests/Browser/Pages/Admin/SharedFolder.php
@@ -1,58 +1,58 @@
 <?php
 
 namespace Tests\Browser\Pages\Admin;
 
 use Laravel\Dusk\Page;
 
 class SharedFolder extends Page
 {
     protected $folderId;
 
     /**
      * Object constructor.
      *
      * @param int $id Shared folder Id
      */
     public function __construct($id)
     {
         $this->folderId = $id;
     }
 
     /**
      * Get the URL for the page.
      *
      * @return string
      */
     public function url(): string
     {
         return '/shared-folder/' . $this->folderId;
     }
 
     /**
      * Assert that the browser is on the page.
      *
      * @param \Laravel\Dusk\Browser $browser The browser object
      *
      * @return void
      */
     public function assert($browser): void
     {
         $browser->waitForLocation($this->url())
             ->waitFor('@folder-info');
     }
 
     /**
      * Get the element shortcuts for the page.
      *
      * @return array
      */
     public function elements(): array
     {
         return [
             '@app' => '#app',
             '@folder-info' => '#folder-info',
-            '@folder-settings' => '#folder-settings',
-            '@folder-aliases' => '#folder-aliases',
+            '@folder-settings' => '#settings',
+            '@folder-aliases' => '#aliases',
         ];
     }
 }
diff --git a/src/tests/Browser/Pages/Admin/User.php b/src/tests/Browser/Pages/Admin/User.php
index c58b774b..08ebb762 100644
--- a/src/tests/Browser/Pages/Admin/User.php
+++ b/src/tests/Browser/Pages/Admin/User.php
@@ -1,67 +1,67 @@
 <?php
 
 namespace Tests\Browser\Pages\Admin;
 
 use Laravel\Dusk\Page;
 
 class User extends Page
 {
     protected $userid;
 
     /**
      * Object constructor.
      *
      * @param int $userid User Id
      */
     public function __construct($userid)
     {
         $this->userid = $userid;
     }
 
     /**
      * Get the URL for the page.
      *
      * @return string
      */
     public function url(): string
     {
         return '/user/' . $this->userid;
     }
 
     /**
      * Assert that the browser is on the page.
      *
      * @param \Laravel\Dusk\Browser $browser The browser object
      *
      * @return void
      */
     public function assert($browser): void
     {
         $browser->waitForLocation($this->url())
             ->waitUntilMissing('@app .app-loader')
             ->waitFor('@user-info');
     }
 
     /**
      * Get the element shortcuts for the page.
      *
      * @return array
      */
     public function elements(): array
     {
         return [
             '@app' => '#app',
             '@user-info' => '#user-info',
             '@nav' => 'ul.nav-tabs',
-            '@user-finances' => '#user-finances',
-            '@user-aliases' => '#user-aliases',
-            '@user-subscriptions' => '#user-subscriptions',
-            '@user-distlists' => '#user-distlists',
-            '@user-domains' => '#user-domains',
-            '@user-resources' => '#user-resources',
-            '@user-shared-folders' => '#user-shared-folders',
-            '@user-users' => '#user-users',
-            '@user-settings' => '#user-settings',
+            '@user-finances' => '#finances',
+            '@user-aliases' => '#aliases',
+            '@user-subscriptions' => '#subscriptions',
+            '@user-distlists' => '#distlists',
+            '@user-domains' => '#domains',
+            '@user-resources' => '#resources',
+            '@user-folders' => '#folders',
+            '@user-users' => '#users',
+            '@user-settings' => '#settings',
         ];
     }
 }
diff --git a/src/tests/Browser/Pages/Wallet.php b/src/tests/Browser/Pages/Wallet.php
index 4f28786d..62aaf253 100644
--- a/src/tests/Browser/Pages/Wallet.php
+++ b/src/tests/Browser/Pages/Wallet.php
@@ -1,49 +1,50 @@
 <?php
 
 namespace Tests\Browser\Pages;
 
 use Laravel\Dusk\Page;
 
 class Wallet extends Page
 {
     /**
      * Get the URL for the page.
      *
      * @return string
      */
     public function url(): string
     {
         return '/wallet';
     }
 
     /**
      * Assert that the browser is on the page.
      *
      * @param \Laravel\Dusk\Browser $browser The browser object
      *
      * @return void
      */
     public function assert($browser)
     {
         $browser->assertPathIs($this->url())
             ->waitUntilMissing('@app .app-loader')
             ->assertSeeIn('#wallet .card-title', 'Account balance');
     }
 
     /**
      * Get the element shortcuts for the page.
      *
      * @return array
      */
     public function elements(): array
     {
         return [
             '@app' => '#app',
             '@main' => '#wallet',
             '@payment-dialog' => '#payment-dialog',
             '@nav' => 'ul.nav-tabs',
-            '@history-tab' => '#wallet-history',
-            '@receipts-tab' => '#wallet-receipts',
+            '@history-tab' => '#history',
+            '@receipts-tab' => '#receipts',
+            '@payments-tab' => '#payments',
         ];
     }
 }
diff --git a/src/tests/Browser/Reseller/PaymentMollieTest.php b/src/tests/Browser/Reseller/PaymentMollieTest.php
index 5859fe94..bdfb7efc 100644
--- a/src/tests/Browser/Reseller/PaymentMollieTest.php
+++ b/src/tests/Browser/Reseller/PaymentMollieTest.php
@@ -1,116 +1,116 @@
 <?php
 
 namespace Tests\Browser\Reseller;
 
 use App\Providers\PaymentProvider;
 use App\Wallet;
 use Tests\Browser;
 use Tests\Browser\Components\Dialog;
 use Tests\Browser\Components\Toast;
 use Tests\Browser\Pages\Dashboard;
 use Tests\Browser\Pages\Home;
 use Tests\Browser\Pages\PaymentMollie;
 use Tests\Browser\Pages\Wallet as WalletPage;
 use Tests\TestCaseDusk;
 
 class PaymentMollieTest extends TestCaseDusk
 {
     /**
      * {@inheritDoc}
      */
     public function setUp(): void
     {
         parent::setUp();
         self::useResellerUrl();
     }
 
     /**
      * {@inheritDoc}
      */
     public function tearDown(): void
     {
         $user = $this->getTestUser('reseller@' . \config('app.domain'));
         $wallet = $user->wallets()->first();
         $wallet->payments()->delete();
         $wallet->balance = 0;
         $wallet->save();
 
         parent::tearDown();
     }
 
     /**
      * Test the payment process
      *
      * @group mollie
      */
     public function testPayment(): void
     {
         $this->browse(function (Browser $browser) {
             $user = $this->getTestUser('reseller@' . \config('app.domain'));
             $wallet = $user->wallets()->first();
             $wallet->payments()->delete();
             $wallet->balance = 0;
             $wallet->save();
 
             $browser->visit(new Home())
                 ->submitLogon($user->email, \App\Utils::generatePassphrase(), true, ['paymentProvider' => 'mollie'])
                 ->on(new Dashboard())
                 ->click('@links .link-wallet')
                 ->on(new WalletPage())
                 ->assertSeeIn('@main button', 'Add credit')
                 ->click('@main button')
                 ->with(new Dialog('@payment-dialog'), function (Browser $browser) {
                     $browser->assertSeeIn('@title', 'Top up your wallet')
-                        ->waitFor('#payment-method-selection #creditcard')
-                        ->waitFor('#payment-method-selection #paypal')
-                        ->waitFor('#payment-method-selection #banktransfer')
-                        ->click('#creditcard');
+                        ->waitFor('#payment-method-selection .link-creditcard svg')
+                        ->waitFor('#payment-method-selection .link-paypal svg')
+                        ->waitFor('#payment-method-selection .link-banktransfer svg')
+                        ->click('#payment-method-selection .link-creditcard');
                 })
                 ->with(new Dialog('@payment-dialog'), function (Browser $browser) {
                     $browser->assertSeeIn('@title', 'Top up your wallet')
                         ->assertFocused('#amount')
                         ->assertSeeIn('@button-cancel', 'Cancel')
                         ->assertSeeIn('@button-action', 'Continue')
                         // Test error handling
                         ->type('@body #amount', 'aaa')
                         ->click('@button-action')
                         ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
                         ->assertSeeIn('#amount + span + .invalid-feedback', 'The amount must be a number.')
                         // Submit valid data
                         ->type('@body #amount', '12.34')
                         // Note we use double click to assert it does not create redundant requests
                         ->click('@button-action')
                         ->click('@button-action');
                 })
                 ->on(new PaymentMollie())
                 ->assertSeeIn('@title', $user->tenant->title . ' Payment')
                 ->assertSeeIn('@amount', 'CHF 12.34');
 
             $this->assertSame(1, $wallet->payments()->count());
 
             // Looks like the Mollie testing mode is limited.
             // We'll select credit card method and mark the payment as paid
             // We can't do much more, we have to trust Mollie their page works ;)
 
             // For some reason I don't get the method selection form, it
             // immediately jumps to the next step. Let's detect that
             if ($browser->element('@methods')) {
                 $browser->click('@methods button.grid-button-creditcard')
                     ->waitFor('button.form__button');
             }
 
             $browser->click('@status-table input[value="paid"]')
                 ->click('button.form__button');
 
             // Now it should redirect back to wallet page and in background
             // use the webhook to update payment status (and balance).
 
             // Looks like in test-mode the webhook is executed before redirect
             // so we can expect balance updated on the wallet page
 
             $browser->waitForLocation('/wallet')
                 ->on(new WalletPage())
                 ->assertSeeIn('@main .card-title', 'Account balance 12,34 CHF');
         });
     }
 }
diff --git a/src/tests/Browser/Reseller/SharedFolderTest.php b/src/tests/Browser/Reseller/SharedFolderTest.php
index 5d1bed71..555915a1 100644
--- a/src/tests/Browser/Reseller/SharedFolderTest.php
+++ b/src/tests/Browser/Reseller/SharedFolderTest.php
@@ -1,109 +1,109 @@
 <?php
 
 namespace Tests\Browser\Reseller;
 
 use App\SharedFolder;
 use Illuminate\Support\Facades\Queue;
 use Tests\Browser;
 use Tests\Browser\Components\Toast;
 use Tests\Browser\Pages\Admin\SharedFolder as SharedFolderPage;
 use Tests\Browser\Pages\Admin\User as UserPage;
 use Tests\Browser\Pages\Dashboard;
 use Tests\Browser\Pages\Home;
 use Tests\TestCaseDusk;
 
 class SharedFolderTest extends TestCaseDusk
 {
     /**
      * {@inheritDoc}
      */
     public function setUp(): void
     {
         parent::setUp();
         self::useResellerUrl();
     }
 
     /**
      * {@inheritDoc}
      */
     public function tearDown(): void
     {
         parent::tearDown();
     }
 
     /**
      * Test shared folder info page (unauthenticated)
      */
     public function testSharedFolderUnauth(): void
     {
         // Test that the page requires authentication
         $this->browse(function (Browser $browser) {
             $user = $this->getTestUser('john@kolab.org');
             $folder = $this->getTestSharedFolder('folder-event@kolab.org');
 
             $browser->visit('/shared-folder/' . $folder->id)->on(new Home());
         });
     }
 
     /**
      * Test shared folder info page
      */
     public function testInfo(): void
     {
         Queue::fake();
 
         $this->browse(function (Browser $browser) {
             $user = $this->getTestUser('john@kolab.org');
             $folder = $this->getTestSharedFolder('folder-event@kolab.org');
             $folder->setConfig(['acl' => ['anyone, read-only', 'jack@kolab.org, read-write']]);
             $folder->setAliases(['folder-alias1@kolab.org', 'folder-alias2@kolab.org']);
             $folder->status = SharedFolder::STATUS_NEW | SharedFolder::STATUS_ACTIVE
                 | SharedFolder::STATUS_LDAP_READY | SharedFolder::STATUS_IMAP_READY;
             $folder->save();
 
             $folder_page = new SharedFolderPage($folder->id);
             $user_page = new UserPage($user->id);
 
             // Goto the folder page
             $browser->visit(new Home())
                 ->submitLogon('reseller@' . \config('app.domain'), \App\Utils::generatePassphrase(), true)
                 ->on(new Dashboard())
                 ->visit($user_page)
                 ->on($user_page)
-                ->click('@nav #tab-shared-folders')
+                ->click('@nav #tab-folders')
                 ->pause(1000)
-                ->click('@user-shared-folders table tbody tr:first-child td:first-child a')
+                ->click('@user-folders table tbody tr:first-child td:first-child a')
                 ->on($folder_page)
                 ->assertSeeIn('@folder-info .card-title', $folder->email)
                 ->with('@folder-info form', function (Browser $browser) use ($folder) {
                     $browser->assertElementsCount('.row', 4)
                         ->assertSeeIn('.row:nth-child(1) label', 'ID (Created)')
                         ->assertSeeIn('.row:nth-child(1) #folderid', "{$folder->id} ({$folder->created_at})")
                         ->assertSeeIn('.row:nth-child(2) label', 'Status')
                         ->assertSeeIn('.row:nth-child(2) #status.text-success', 'Active')
                         ->assertSeeIn('.row:nth-child(3) label', 'Name')
                         ->assertSeeIn('.row:nth-child(3) #name', $folder->name)
                         ->assertSeeIn('.row:nth-child(4) label', 'Type')
                         ->assertSeeIn('.row:nth-child(4) #type', 'Calendar');
                 })
                 ->assertElementsCount('ul.nav-tabs .nav-item', 2)
                 ->assertSeeIn('ul.nav-tabs .nav-item:nth-child(1) .nav-link', 'Settings')
                 ->with('@folder-settings form', function (Browser $browser) {
                     $browser->assertElementsCount('.row', 1)
                         ->assertSeeIn('.row:nth-child(1) label', 'Access rights')
                         ->assertSeeIn('.row:nth-child(1) #acl', 'anyone: read-only')
                         ->assertSeeIn('.row:nth-child(1) #acl', 'jack@kolab.org: read-write');
                 })
                 ->assertSeeIn('ul.nav-tabs .nav-item:nth-child(2) .nav-link', 'Email Aliases (2)')
                 ->click('ul.nav-tabs .nav-item:nth-child(2) .nav-link')
                 ->with('@folder-aliases table', function (Browser $browser) {
                     $browser->assertElementsCount('tbody tr', 2)
                         ->assertSeeIn('tbody tr:nth-child(1) td', 'folder-alias1@kolab.org')
                         ->assertSeeIn('tbody tr:nth-child(2) td', 'folder-alias2@kolab.org');
                 });
 
             // Test invalid shared folder identifier
             $browser->visit('/shared-folder/abc')->assertErrorPage(404);
         });
     }
 }
diff --git a/src/tests/Browser/Reseller/UserTest.php b/src/tests/Browser/Reseller/UserTest.php
index c891975e..46b4dd0a 100644
--- a/src/tests/Browser/Reseller/UserTest.php
+++ b/src/tests/Browser/Reseller/UserTest.php
@@ -1,577 +1,577 @@
 <?php
 
 namespace Tests\Browser\Reseller;
 
 use App\Auth\SecondFactor;
 use App\Discount;
 use App\Sku;
 use App\User;
 use Tests\Browser;
 use Tests\Browser\Components\Dialog;
 use Tests\Browser\Components\Toast;
 use Tests\Browser\Pages\Admin\User as UserPage;
 use Tests\Browser\Pages\Dashboard;
 use Tests\Browser\Pages\Home;
 use Tests\TestCaseDusk;
 use Illuminate\Foundation\Testing\DatabaseMigrations;
 
 class UserTest extends TestCaseDusk
 {
     /**
      * {@inheritDoc}
      */
     public function setUp(): void
     {
         parent::setUp();
         self::useResellerUrl();
 
         $john = $this->getTestUser('john@kolab.org');
         $john->setSettings([
                 'phone' => '+48123123123',
                 'external_email' => 'john.doe.external@gmail.com',
         ]);
         if ($john->isSuspended()) {
             User::where('email', $john->email)->update(['status' => $john->status - User::STATUS_SUSPENDED]);
         }
         $wallet = $john->wallets()->first();
         $wallet->discount()->dissociate();
         $wallet->save();
 
         $this->deleteTestGroup('group-test@kolab.org');
         $this->clearMeetEntitlements();
     }
 
     /**
      * {@inheritDoc}
      */
     public function tearDown(): void
     {
         $john = $this->getTestUser('john@kolab.org');
         $john->setSettings([
                 'phone' => null,
                 'external_email' => 'john.doe.external@gmail.com',
         ]);
         if ($john->isSuspended()) {
             User::where('email', $john->email)->update(['status' => $john->status - User::STATUS_SUSPENDED]);
         }
         $wallet = $john->wallets()->first();
         $wallet->discount()->dissociate();
         $wallet->save();
 
         $this->deleteTestGroup('group-test@kolab.org');
         $this->clearMeetEntitlements();
 
         parent::tearDown();
     }
 
     /**
      * Test user info page (unauthenticated)
      */
     public function testUserUnauth(): void
     {
         // Test that the page requires authentication
         $this->browse(function (Browser $browser) {
             $jack = $this->getTestUser('jack@kolab.org');
             $browser->visit('/user/' . $jack->id)->on(new Home());
         });
     }
 
     /**
      * Test user info page
      */
     public function testUserInfo(): void
     {
         $this->browse(function (Browser $browser) {
             $jack = $this->getTestUser('jack@kolab.org');
             $page = new UserPage($jack->id);
 
             $browser->visit(new Home())
                 ->submitLogon('reseller@' . \config('app.domain'), \App\Utils::generatePassphrase(), true)
                 ->on(new Dashboard())
                 ->visit($page)
                 ->on($page);
 
             // Assert main info box content
             $browser->assertSeeIn('@user-info .card-title', $jack->email)
                 ->with('@user-info form', function (Browser $browser) use ($jack) {
                     $browser->assertElementsCount('.row', 7)
                         ->assertSeeIn('.row:nth-child(1) label', 'Managed by')
                         ->assertSeeIn('.row:nth-child(1) #manager a', 'john@kolab.org')
                         ->assertSeeIn('.row:nth-child(2) label', 'ID (Created)')
                         ->assertSeeIn('.row:nth-child(2) #userid', "{$jack->id} ({$jack->created_at})")
                         ->assertSeeIn('.row:nth-child(3) label', 'Status')
                         ->assertSeeIn('.row:nth-child(3) #status span.text-success', 'Active')
                         ->assertSeeIn('.row:nth-child(4) label', 'First Name')
                         ->assertSeeIn('.row:nth-child(4) #first_name', 'Jack')
                         ->assertSeeIn('.row:nth-child(5) label', 'Last Name')
                         ->assertSeeIn('.row:nth-child(5) #last_name', 'Daniels')
                         ->assertSeeIn('.row:nth-child(6) label', 'External Email')
                         ->assertMissing('.row:nth-child(6) #external_email a')
                         ->assertSeeIn('.row:nth-child(7) label', 'Country')
                         ->assertSeeIn('.row:nth-child(7) #country', 'United States');
                 });
 
             // Some tabs are loaded in background, wait a second
             $browser->pause(500)
                 ->assertElementsCount('@nav a', 9);
 
             // Note: Finances tab is tested in UserFinancesTest.php
             $browser->assertSeeIn('@nav #tab-finances', 'Finances');
 
             // Assert Aliases tab
             $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (1)')
                 ->click('@nav #tab-aliases')
                 ->whenAvailable('@user-aliases', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 1)
                         ->assertSeeIn('table tbody tr:first-child td:first-child', 'jack.daniels@kolab.org')
                         ->assertMissing('table tfoot');
                 });
 
             // Assert Subscriptions tab
             $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (3)')
                 ->click('@nav #tab-subscriptions')
                 ->with('@user-subscriptions', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 3)
                         ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox')
                         ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '5,00 CHF/month')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 5 GB')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month')
                         ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
                         ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,90 CHF/month')
                         ->assertMissing('table tfoot')
                         ->assertMissing('#reset2fa');
                 });
 
             // Assert Domains tab
             $browser->assertSeeIn('@nav #tab-domains', 'Domains (0)')
                 ->click('@nav #tab-domains')
                 ->with('@user-domains', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no domains in this account.');
                 });
 
             // Assert Users tab
             $browser->assertSeeIn('@nav #tab-users', 'Users (0)')
                 ->click('@nav #tab-users')
                 ->with('@user-users', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no users in this account.');
                 });
 
             // Assert Distribution lists tab
             $browser->assertSeeIn('@nav #tab-distlists', 'Distribution lists (0)')
                 ->click('@nav #tab-distlists')
                 ->with('@user-distlists', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no distribution lists in this account.');
                 });
 
             // Assert Resources tab
             $browser->assertSeeIn('@nav #tab-resources', 'Resources (0)')
                 ->click('@nav #tab-resources')
                 ->with('@user-resources', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no resources in this account.');
                 });
 
             // Assert Shared folders tab
-            $browser->assertSeeIn('@nav #tab-shared-folders', 'Shared folders (0)')
-                ->click('@nav #tab-shared-folders')
-                ->with('@user-shared-folders', function (Browser $browser) {
+            $browser->assertSeeIn('@nav #tab-folders', 'Shared folders (0)')
+                ->click('@nav #tab-folders')
+                ->with('@user-folders', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no shared folders in this account.');
                 });
 
             // Assert Settings tab
             $browser->assertSeeIn('@nav #tab-settings', 'Settings')
                 ->click('@nav #tab-settings')
                 ->whenAvailable('@user-settings form', function (Browser $browser) {
                     $browser->assertElementsCount('.row', 1)
                         ->assertSeeIn('.row:first-child label', 'Greylisting')
                         ->assertSeeIn('.row:first-child .text-success', 'enabled');
                 });
         });
     }
 
     /**
      * Test user info page (continue)
      *
      * @depends testUserInfo
      */
     public function testUserInfo2(): void
     {
         $this->browse(function (Browser $browser) {
             $john = $this->getTestUser('john@kolab.org');
             $page = new UserPage($john->id);
             $discount = Discount::where('code', 'TEST')->first();
             $wallet = $john->wallet();
             $wallet->discount()->associate($discount);
             $wallet->debit(2010);
             $wallet->save();
             $group = $this->getTestGroup('group-test@kolab.org', ['name' => 'Test Group']);
             $group->assignToWallet($john->wallets->first());
 
             // Click the managed-by link on Jack's page
             $browser->click('@user-info #manager a')
                 ->on($page);
 
             // Assert main info box content
             $browser->assertSeeIn('@user-info .card-title', $john->email)
                 ->with('@user-info form', function (Browser $browser) use ($john) {
                     $ext_email = $john->getSetting('external_email');
 
                     $browser->assertElementsCount('.row', 9)
                         ->assertSeeIn('.row:nth-child(1) label', 'ID (Created)')
                         ->assertSeeIn('.row:nth-child(1) #userid', "{$john->id} ({$john->created_at})")
                         ->assertSeeIn('.row:nth-child(2) label', 'Status')
                         ->assertSeeIn('.row:nth-child(2) #status span.text-success', 'Active')
                         ->assertSeeIn('.row:nth-child(3) label', 'First Name')
                         ->assertSeeIn('.row:nth-child(3) #first_name', 'John')
                         ->assertSeeIn('.row:nth-child(4) label', 'Last Name')
                         ->assertSeeIn('.row:nth-child(4) #last_name', 'Doe')
                         ->assertSeeIn('.row:nth-child(5) label', 'Organization')
                         ->assertSeeIn('.row:nth-child(5) #organization', 'Kolab Developers')
                         ->assertSeeIn('.row:nth-child(6) label', 'Phone')
                         ->assertSeeIn('.row:nth-child(6) #phone', $john->getSetting('phone'))
                         ->assertSeeIn('.row:nth-child(7) label', 'External Email')
                         ->assertSeeIn('.row:nth-child(7) #external_email a', $ext_email)
                         ->assertAttribute('.row:nth-child(7) #external_email a', 'href', "mailto:$ext_email")
                         ->assertSeeIn('.row:nth-child(8) label', 'Address')
                         ->assertSeeIn('.row:nth-child(8) #billing_address', $john->getSetting('billing_address'))
                         ->assertSeeIn('.row:nth-child(9) label', 'Country')
                         ->assertSeeIn('.row:nth-child(9) #country', 'United States');
                 });
 
             // Some tabs are loaded in background, wait a second
             $browser->pause(500)
                 ->assertElementsCount('@nav a', 9);
 
             // Note: Finances tab is tested in UserFinancesTest.php
             $browser->assertSeeIn('@nav #tab-finances', 'Finances');
 
             // Assert Aliases tab
             $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (1)')
                 ->click('@nav #tab-aliases')
                 ->whenAvailable('@user-aliases', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 1)
                         ->assertSeeIn('table tbody tr:first-child td:first-child', 'john.doe@kolab.org')
                         ->assertMissing('table tfoot');
                 });
 
             // Assert Subscriptions tab
             $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (3)')
                 ->click('@nav #tab-subscriptions')
                 ->with('@user-subscriptions', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 3)
                         ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox')
                         ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,50 CHF/month¹')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 5 GB')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹')
                         ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
                         ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,41 CHF/month¹')
                         ->assertMissing('table tfoot')
                         ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher');
                 });
 
             // Assert Users tab
             $browser->assertSeeIn('@nav #tab-users', 'Users (4)')
                 ->click('@nav #tab-users')
                 ->with('@user-users table', function (Browser $browser) {
                     $browser->assertElementsCount('tbody tr', 4)
                         ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'jack@kolab.org')
                         ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-success')
                         ->assertSeeIn('tbody tr:nth-child(2) td:first-child a', 'joe@kolab.org')
                         ->assertVisible('tbody tr:nth-child(2) td:first-child svg.text-success')
                         ->assertSeeIn('tbody tr:nth-child(3) td:first-child span', 'john@kolab.org')
                         ->assertVisible('tbody tr:nth-child(3) td:first-child svg.text-success')
                         ->assertSeeIn('tbody tr:nth-child(4) td:first-child a', 'ned@kolab.org')
                         ->assertVisible('tbody tr:nth-child(4) td:first-child svg.text-success')
                         ->assertMissing('tfoot');
                 });
 
             // Assert Domains tab
             $browser->assertSeeIn('@nav #tab-domains', 'Domains (1)')
                 ->click('@nav #tab-domains')
                 ->with('@user-domains table', function (Browser $browser) {
                     $browser->assertElementsCount('tbody tr', 1)
                         ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'kolab.org')
                         ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-success')
                         ->assertMissing('tfoot');
                 });
 
             // Assert Distribution lists tab
             $browser->assertSeeIn('@nav #tab-distlists', 'Distribution lists (1)')
                 ->click('@nav #tab-distlists')
                 ->with('@user-distlists table', function (Browser $browser) {
                     $browser->assertElementsCount('tbody tr', 1)
                         ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'Test Group')
                         ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-danger')
                         ->assertSeeIn('tbody tr:nth-child(1) td:last-child a', 'group-test@kolab.org')
                         ->assertMissing('tfoot');
                 });
 
             // Assert Resources tab
             $browser->assertSeeIn('@nav #tab-resources', 'Resources (2)')
                 ->click('@nav #tab-resources')
                 ->with('@user-resources', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 2)
                         ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'Conference Room #1')
                         ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', 'resource-test1@kolab.org')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Conference Room #2')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', 'resource-test2@kolab.org')
                         ->assertMissing('table tfoot');
                 });
 
             // Assert Shared folders tab
-            $browser->assertSeeIn('@nav #tab-shared-folders', 'Shared folders (2)')
-                ->click('@nav #tab-shared-folders')
-                ->with('@user-shared-folders', function (Browser $browser) {
+            $browser->assertSeeIn('@nav #tab-folders', 'Shared folders (2)')
+                ->click('@nav #tab-folders')
+                ->with('@user-folders', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 2)
                         ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'Calendar')
                         ->assertSeeIn('table tbody tr:nth-child(1) td:nth-child(2)', 'Calendar')
                         ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', 'folder-event@kolab.org')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Contacts')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:nth-child(2)', 'Address Book')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', 'folder-contact@kolab.org')
                         ->assertMissing('table tfoot');
                 });
         });
 
         // Now we go to Ned's info page, he's a controller on John's wallet
         $this->browse(function (Browser $browser) {
             $ned = $this->getTestUser('ned@kolab.org');
             $ned->setSetting('greylist_enabled', 'false');
             $page = new UserPage($ned->id);
 
             $browser->click('@nav #tab-users')
                 ->click('@user-users tbody tr:nth-child(4) td:first-child a')
                 ->on($page);
 
             // Assert main info box content
             $browser->assertSeeIn('@user-info .card-title', $ned->email)
                 ->with('@user-info form', function (Browser $browser) use ($ned) {
                     $browser->assertSeeIn('.row:nth-child(2) label', 'ID (Created)')
                         ->assertSeeIn('.row:nth-child(2) #userid', "{$ned->id} ({$ned->created_at})");
                 });
 
             // Some tabs are loaded in background, wait a second
             $browser->pause(500)
                 ->assertElementsCount('@nav a', 9);
 
             // Note: Finances tab is tested in UserFinancesTest.php
             $browser->assertSeeIn('@nav #tab-finances', 'Finances');
 
             // Assert Aliases tab
             $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (0)')
                 ->click('@nav #tab-aliases')
                 ->whenAvailable('@user-aliases', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'This user has no email aliases.');
                 });
 
             // Assert Subscriptions tab, we expect John's discount here
             $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (5)')
                 ->click('@nav #tab-subscriptions')
                 ->with('@user-subscriptions', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 5)
                         ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox')
                         ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,50 CHF/month¹')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 5 GB')
                         ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹')
                         ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
                         ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,41 CHF/month¹')
                         ->assertSeeIn('table tbody tr:nth-child(4) td:first-child', 'Activesync')
                         ->assertSeeIn('table tbody tr:nth-child(4) td:last-child', '0,00 CHF/month¹')
                         ->assertSeeIn('table tbody tr:nth-child(5) td:first-child', '2-Factor Authentication')
                         ->assertSeeIn('table tbody tr:nth-child(5) td:last-child', '0,00 CHF/month¹')
                         ->assertMissing('table tfoot')
                         ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher')
                         ->assertSeeIn('#reset2fa', 'Reset 2-Factor Auth');
                 });
 
             // We don't expect John's domains here
             $browser->assertSeeIn('@nav #tab-domains', 'Domains (0)')
                 ->click('@nav #tab-domains')
                 ->with('@user-domains', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no domains in this account.');
                 });
 
             // We don't expect John's users here
             $browser->assertSeeIn('@nav #tab-users', 'Users (0)')
                 ->click('@nav #tab-users')
                 ->with('@user-users', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no users in this account.');
                 });
 
             // We don't expect John's distribution lists here
             $browser->assertSeeIn('@nav #tab-distlists', 'Distribution lists (0)')
                 ->click('@nav #tab-distlists')
                 ->with('@user-distlists', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no distribution lists in this account.');
                 });
 
             // Assert Resources tab
             $browser->assertSeeIn('@nav #tab-resources', 'Resources (0)')
                 ->click('@nav #tab-resources')
                 ->with('@user-resources', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no resources in this account.');
                 });
 
             // Assert Shared folders tab
-            $browser->assertSeeIn('@nav #tab-shared-folders', 'Shared folders (0)')
-                ->click('@nav #tab-shared-folders')
-                ->with('@user-shared-folders', function (Browser $browser) {
+            $browser->assertSeeIn('@nav #tab-folders', 'Shared folders (0)')
+                ->click('@nav #tab-folders')
+                ->with('@user-folders', function (Browser $browser) {
                     $browser->assertElementsCount('table tbody tr', 0)
                         ->assertSeeIn('table tfoot tr td', 'There are no shared folders in this account.');
                 });
 
             // Assert Settings tab
             $browser->assertSeeIn('@nav #tab-settings', 'Settings')
                 ->click('@nav #tab-settings')
                 ->whenAvailable('@user-settings form', function (Browser $browser) {
                     $browser->assertElementsCount('.row', 1)
                         ->assertSeeIn('.row:first-child label', 'Greylisting')
                         ->assertSeeIn('.row:first-child .text-danger', 'disabled');
                 });
         });
     }
 
     /**
      * Test editing an external email
      *
      * @depends testUserInfo2
      */
     public function testExternalEmail(): void
     {
         $this->browse(function (Browser $browser) {
             $john = $this->getTestUser('john@kolab.org');
 
             $browser->visit(new UserPage($john->id))
                 ->waitFor('@user-info #external_email button')
                 ->click('@user-info #external_email button')
                 // Test dialog content, and closing it with Cancel button
                 ->with(new Dialog('#email-dialog'), function (Browser $browser) {
                     $browser->assertSeeIn('@title', 'External Email')
                         ->assertFocused('@body input')
                         ->assertValue('@body input', 'john.doe.external@gmail.com')
                         ->assertSeeIn('@button-cancel', 'Cancel')
                         ->assertSeeIn('@button-action', 'Submit')
                         ->click('@button-cancel');
                 })
                 ->assertMissing('#email-dialog')
                 ->click('@user-info #external_email button')
                 // Test email validation error handling, and email update
                 ->with(new Dialog('#email-dialog'), function (Browser $browser) {
                     $browser->type('@body input', 'test')
                         ->click('@button-action')
                         ->waitFor('@body input.is-invalid')
                         ->assertSeeIn(
                             '@body input + .invalid-feedback',
                             'The external email must be a valid email address.'
                         )
                         ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
                         ->type('@body input', 'test@test.com')
                         ->click('@button-action');
                 })
                 ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.')
                 ->assertSeeIn('@user-info #external_email a', 'test@test.com')
                 ->click('@user-info #external_email button')
                 ->with(new Dialog('#email-dialog'), function (Browser $browser) {
                     $browser->assertValue('@body input', 'test@test.com')
                         ->assertMissing('@body input.is-invalid')
                         ->assertMissing('@body input + .invalid-feedback')
                         ->click('@button-cancel');
                 })
                 ->assertSeeIn('@user-info #external_email a', 'test@test.com');
 
             // $john->getSetting() may not work here as it uses internal cache
             // read the value form database
             $current_ext_email = $john->settings()->where('key', 'external_email')->first()->value;
             $this->assertSame('test@test.com', $current_ext_email);
         });
     }
 
     /**
      * Test suspending/unsuspending the user
      */
     public function testSuspendAndUnsuspend(): void
     {
         $this->browse(function (Browser $browser) {
             $john = $this->getTestUser('john@kolab.org');
 
             $browser->visit(new UserPage($john->id))
                 ->assertVisible('@user-info #button-suspend')
                 ->assertMissing('@user-info #button-unsuspend')
                 ->click('@user-info #button-suspend')
                 ->assertToast(Toast::TYPE_SUCCESS, 'User suspended successfully.')
                 ->assertSeeIn('@user-info #status span.text-warning', 'Suspended')
                 ->assertMissing('@user-info #button-suspend')
                 ->click('@user-info #button-unsuspend')
                 ->assertToast(Toast::TYPE_SUCCESS, 'User unsuspended successfully.')
                 ->assertSeeIn('@user-info #status span.text-success', 'Active')
                 ->assertVisible('@user-info #button-suspend')
                 ->assertMissing('@user-info #button-unsuspend');
         });
     }
 
     /**
      * Test resetting 2FA for the user
      */
     public function testReset2FA(): void
     {
         $this->browse(function (Browser $browser) {
             $this->deleteTestUser('userstest1@kolabnow.com');
             $user = $this->getTestUser('userstest1@kolabnow.com');
             $sku2fa = Sku::withEnvTenantContext()->where(['title' => '2fa'])->first();
             $user->assignSku($sku2fa);
             SecondFactor::seed('userstest1@kolabnow.com');
 
             $browser->visit(new UserPage($user->id))
                 ->click('@nav #tab-subscriptions')
                 ->with('@user-subscriptions', function (Browser $browser) use ($sku2fa) {
                     $browser->waitFor('#reset2fa')
                         ->assertVisible('#sku' . $sku2fa->id);
                 })
                 ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (1)')
                 ->click('#reset2fa')
                 ->with(new Dialog('#reset-2fa-dialog'), function (Browser $browser) {
                     $browser->assertSeeIn('@title', '2-Factor Authentication Reset')
                         ->assertSeeIn('@button-cancel', 'Cancel')
                         ->assertSeeIn('@button-action', 'Reset')
                         ->click('@button-action');
                 })
                 ->assertToast(Toast::TYPE_SUCCESS, '2-Factor authentication reset successfully.')
                 ->assertMissing('#sku' . $sku2fa->id)
                 ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (0)');
         });
     }
 
     /**
      * Test adding the beta SKU for the user
      */
     public function testAddBetaSku(): void
     {
         $this->browse(function (Browser $browser) {
             $this->deleteTestUser('userstest1@kolabnow.com');
             $user = $this->getTestUser('userstest1@kolabnow.com');
             $sku = Sku::withEnvTenantContext()->where('title', 'beta')->first();
 
             $browser->visit(new UserPage($user->id))
                 ->click('@nav #tab-subscriptions')
                 ->waitFor('@user-subscriptions #addbetasku')
                 ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (0)')
                 ->assertSeeIn('#addbetasku', 'Enable beta program')
                 ->click('#addbetasku')
                 ->assertToast(Toast::TYPE_SUCCESS, 'The subscription added successfully.')
                 ->waitFor('#sku' . $sku->id)
                 ->assertSeeIn("#sku{$sku->id} td:first-child", 'Private Beta (invitation only)')
                 ->assertSeeIn("#sku{$sku->id} td:last-child", '0,00 CHF/month')
                 ->assertMissing('#addbetasku')
                 ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (1)');
         });
     }
 }