+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/teams/TeamForm.vue b/app/javascript/dashboard/routes/dashboard/settings/teams/TeamForm.vue
index 0773e0d08..0a231568a 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/teams/TeamForm.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/teams/TeamForm.vue
@@ -1,15 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('ONBOARDING_INBOX_SETUP.CHANNELS.HEADER') }}
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/Index.vue b/app/javascript/dashboard/routes/dashboard/onboarding/Index.vue
index 3a1d65bc6..7e4fbc84e 100644
--- a/app/javascript/dashboard/routes/dashboard/onboarding/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/onboarding/Index.vue
@@ -1,21 +1,22 @@
+
+
+
+
+
+
+
+ {{ t(channel.labelKey) }}
+
+
+ {{ connectedName }}
+
+
+ {{ t('ONBOARDING_INBOX_SETUP.CHANNELS.CONNECTED') }}
+
+
+
+
+ {{ t('ONBOARDING_INBOX_SETUP.CHANNELS.CONNECT') }}
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/CreationStatusRow.vue b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/CreationStatusRow.vue
new file mode 100644
index 000000000..c0157398e
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/CreationStatusRow.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+ {{ description }}
+
+
{{ status }}
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/HelpCenterCreationStatus.vue b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/HelpCenterCreationStatus.vue
new file mode 100644
index 000000000..8c0e1decc
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/HelpCenterCreationStatus.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/InboxChannelForm.vue b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/InboxChannelForm.vue
new file mode 100644
index 000000000..45f170137
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/InboxChannelForm.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+ {{ field.label }}
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/InboxChannelsDialog.vue b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/InboxChannelsDialog.vue
new file mode 100644
index 000000000..a3631649d
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/InboxChannelsDialog.vue
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t(channel.labelKey) }}
+
+
+ {{ t('ONBOARDING_INBOX_SETUP.CHANNELS_DIALOG.SETUP_LATER') }}
+
+
+
+
+
+
+
+ {{ t('ONBOARDING_INBOX_SETUP.CHANNELS_DIALOG.NOTE') }}
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/InboxChannelsFooter.vue b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/InboxChannelsFooter.vue
new file mode 100644
index 000000000..b5c138c65
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/InboxChannelsFooter.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+ {{ t('ONBOARDING_INBOX_SETUP.CHANNELS.MORE_CHANNELS_EMAIL') }}
+
+
+
+
+ {{ t('ONBOARDING_INBOX_SETUP.CHANNELS.MORE_CHANNELS_VOICE') }}
+
+
+
+
+
+
+
+
+
+ {{ t('ONBOARDING_INBOX_SETUP.CHANNELS.VIEW_ALL') }}
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/InboxFacebookForm.vue b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/InboxFacebookForm.vue
new file mode 100644
index 000000000..48be07821
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/InboxFacebookForm.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+ {{ t('ONBOARDING_INBOX_SETUP.CHANNELS_DIALOG.FACEBOOK_LOADING') }}
+
+
+
+
+
+
+ {{ t('ONBOARDING_INBOX_SETUP.CHANNELS_DIALOG.FACEBOOK_SELECT_PAGE') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('ONBOARDING_INBOX_SETUP.CHANNELS_DIALOG.FACEBOOK_NO_PAGES') }}
+
+
+
+
+
+
+
+
+ {{ t('ONBOARDING_INBOX_SETUP.CHANNELS_DIALOG.FACEBOOK_ERROR') }}
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/WebWidgetCreationStatus.vue b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/WebWidgetCreationStatus.vue
new file mode 100644
index 000000000..aeb40e24c
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/WebWidgetCreationStatus.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/channelMatchers.js b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/channelMatchers.js
new file mode 100644
index 000000000..0ebdd728f
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/channelMatchers.js
@@ -0,0 +1,18 @@
+import { INBOX_TYPES } from 'dashboard/helper/inbox';
+
+// A detected channel maps to a real inbox when they share a channel_type. Gmail
+// and Outlook both use Channel::Email, so for email we also match on provider.
+// `stub` is a channel's `{ channel_type, provider }` shape (e.g. channel.inbox).
+
+// Returns the matching inbox (not a boolean) so callers can show the connected
+// account's real name rather than the detected handle.
+export const findConnectedInbox = (inboxes, stub) =>
+ inboxes.find(
+ inbox =>
+ inbox.channel_type === stub?.channel_type &&
+ (stub?.channel_type !== INBOX_TYPES.EMAIL ||
+ inbox.provider === stub?.provider)
+ );
+
+export const isChannelConnected = (inboxes, stub) =>
+ Boolean(stub) && Boolean(findConnectedInbox(inboxes, stub));
diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/constants.js b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/constants.js
new file mode 100644
index 000000000..91e800d06
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/constants.js
@@ -0,0 +1,149 @@
+import { CHANNEL_TYPES } from 'dashboard/helper/inbox';
+
+// Channels whose connect flow opens the channels dialog preselected to their
+// in-dialog step — Facebook (page picker) and the credential-form channels
+// (Telegram, Line) — rather than redirecting through OAuth.
+export const DIALOG_CHANNELS = [
+ CHANNEL_TYPES.FACEBOOK,
+ CHANNEL_TYPES.TELEGRAM,
+ CHANNEL_TYPES.LINE,
+];
+
+// Suggested channels (in priority order) to offer as rows when nothing is
+// detected, so the step isn't empty. The mainstream OAuth channels show on
+// configured installs, while credential-free Telegram/LINE keep the list
+// non-empty on a bare self-host.
+export const DEFAULT_CHANNEL_TYPES = [
+ CHANNEL_TYPES.WHATSAPP,
+ CHANNEL_TYPES.FACEBOOK,
+ CHANNEL_TYPES.INSTAGRAM,
+ CHANNEL_TYPES.TELEGRAM,
+ CHANNEL_TYPES.LINE,
+];
+
+// Channels offered in the onboarding "View all" dialog. `inbox` is a stub shaped
+// like a real inbox so ChannelIcon can resolve the icon from the shared provider.
+// With `use-brand-icon`, ChannelIcon renders the full-color brand logo when one
+// exists and falls back to the monochrome glyph otherwise, so no per-channel
+// style flag is needed. Entries without a channel type (Voice, Other Email
+// Providers) render `fallbackIcon` instead. `form: true` swaps the grid for an
+// inline credential form; `setupLater: true` defers the channel to in-app setup
+// for this phase. `labelKey` is an i18n key — most reuse the shared channel
+// titles from the inbox settings (INBOX_MGMT.ADD.AUTH.CHANNEL.*.TITLE) so the
+// names translate without duplicating strings; resolve it with `t()` at display.
+export const CHANNEL_LIST = [
+ {
+ type: 'website',
+ labelKey: 'INBOX_MGMT.ADD.AUTH.CHANNEL.WEBSITE.TITLE',
+ inbox: { channel_type: 'Channel::WebWidget' },
+ },
+ {
+ type: 'whatsapp',
+ labelKey: 'INBOX_MGMT.ADD.AUTH.CHANNEL.WHATSAPP.TITLE',
+ inbox: { channel_type: 'Channel::Whatsapp' },
+ },
+ {
+ type: 'instagram',
+ labelKey: 'INBOX_MGMT.ADD.AUTH.CHANNEL.INSTAGRAM.TITLE',
+ inbox: { channel_type: 'Channel::Instagram' },
+ },
+ {
+ type: 'facebook',
+ labelKey: 'INBOX_MGMT.ADD.AUTH.CHANNEL.FACEBOOK.TITLE',
+ inbox: { channel_type: 'Channel::FacebookPage' },
+ },
+ {
+ type: 'tiktok',
+ labelKey: 'INBOX_MGMT.ADD.AUTH.CHANNEL.TIKTOK.TITLE',
+ inbox: { channel_type: 'Channel::Tiktok' },
+ },
+ {
+ type: 'telegram',
+ labelKey: 'INBOX_MGMT.ADD.AUTH.CHANNEL.TELEGRAM.TITLE',
+ inbox: { channel_type: 'Channel::Telegram' },
+ form: true,
+ },
+ {
+ type: 'line',
+ labelKey: 'INBOX_MGMT.ADD.AUTH.CHANNEL.LINE.TITLE',
+ inbox: { channel_type: 'Channel::Line' },
+ form: true,
+ },
+ // Email channels (including Gmail/Outlook OAuth) are set up later in-app for
+ // this phase; they will be enabled in a future PR.
+ {
+ type: 'gmail',
+ labelKey: 'ONBOARDING_INBOX_SETUP.CHANNELS.GMAIL',
+ inbox: { channel_type: 'Channel::Email', provider: 'google' },
+ setupLater: true,
+ },
+ {
+ type: 'outlook',
+ labelKey: 'ONBOARDING_INBOX_SETUP.CHANNELS.OUTLOOK',
+ inbox: { channel_type: 'Channel::Email', provider: 'microsoft' },
+ setupLater: true,
+ },
+ {
+ type: 'sms',
+ labelKey: 'INBOX_MGMT.ADD.AUTH.CHANNEL.SMS.TITLE',
+ inbox: { channel_type: 'Channel::Sms' },
+ setupLater: true,
+ },
+ {
+ type: 'api',
+ labelKey: 'INBOX_MGMT.ADD.AUTH.CHANNEL.API.TITLE',
+ inbox: { channel_type: 'Channel::Api' },
+ setupLater: true,
+ },
+ {
+ type: 'voice',
+ labelKey: 'INBOX_MGMT.ADD.AUTH.CHANNEL.VOICE.TITLE',
+ fallbackIcon: 'i-woot-voice',
+ setupLater: true,
+ },
+ {
+ type: 'email',
+ labelKey: 'ONBOARDING_INBOX_SETUP.CHANNELS.OTHER_EMAIL',
+ fallbackIcon: 'i-woot-mail',
+ setupLater: true,
+ },
+];
+
+const channelByType = type =>
+ CHANNEL_LIST.find(channel => channel.type === type);
+
+// Icons shown next to "View all" when every detected channel is already
+// connected — a representative trio sourced from CHANNEL_LIST so the inbox stubs
+// aren't duplicated.
+export const FALLBACK_PREVIEW_CHANNELS = ['gmail', 'tiktok', 'whatsapp'].map(
+ channelByType
+);
+
+// Social channels that detected brand_info socials map to, keyed by social type
+// in the order they're offered as rows. Derived from CHANNEL_LIST so channel
+// identity (label, channel_type) has a single source. Keys mirror
+// SocialLinkParser::SOCIAL_DOMAIN_MAP.
+const SOCIAL_PLATFORM_TYPES = [
+ 'whatsapp',
+ 'facebook',
+ 'line',
+ 'instagram',
+ 'telegram',
+ 'tiktok',
+];
+
+export const SOCIAL_PLATFORMS = Object.fromEntries(
+ SOCIAL_PLATFORM_TYPES.map(type => {
+ const { labelKey, inbox } = channelByType(type);
+ return [type, { labelKey, channelType: inbox.channel_type }];
+ })
+);
+
+// Mailbox providers inferred from the signup domain's MX records, keyed by
+// Channel::Email#provider. Derived from CHANNEL_LIST's email entries.
+export const EMAIL_PROVIDERS = Object.fromEntries(
+ CHANNEL_LIST.filter(channel => channel.inbox?.provider).map(channel => [
+ channel.inbox.provider,
+ { labelKey: channel.labelKey },
+ ])
+);
diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useChannelConfig.js b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useChannelConfig.js
new file mode 100644
index 000000000..ba2d6f0dc
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useChannelConfig.js
@@ -0,0 +1,29 @@
+import { useMapGetter } from 'dashboard/composables/store';
+
+// OAuth/SDK channels need installation-level app credentials to be usable. When
+// the credential is missing the channel is "not configured" and is hidden from
+// onboarding entirely. Channels without an entry (Website, Telegram, Line, …)
+// need no installation credential and are always considered configured.
+// Mirrors the availability checks in ChannelItem.vue.
+export function useChannelConfig() {
+ const globalConfig = useMapGetter('globalConfig/get');
+ const installationConfig = window.chatwootConfig || {};
+
+ const CHANNEL_CONFIGURED = {
+ // WhatsApp is onboarded only via Meta embedded signup, which needs both the
+ // app id (not the 'none' sentinel) and the signup configuration id.
+ whatsapp: () =>
+ Boolean(installationConfig.whatsappAppId) &&
+ installationConfig.whatsappAppId !== 'none' &&
+ Boolean(installationConfig.whatsappConfigurationId),
+ facebook: () => Boolean(installationConfig.fbAppId),
+ instagram: () => Boolean(installationConfig.instagramAppId),
+ tiktok: () => Boolean(installationConfig.tiktokAppId),
+ gmail: () => Boolean(installationConfig.googleOAuthClientId),
+ outlook: () => Boolean(globalConfig.value.azureAppId),
+ };
+
+ const isConfigured = type => CHANNEL_CONFIGURED[type]?.() ?? true;
+
+ return { isConfigured };
+}
diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useChannelConnect.js b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useChannelConnect.js
new file mode 100644
index 000000000..bd34d5f20
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useChannelConnect.js
@@ -0,0 +1,67 @@
+import { useI18n } from 'vue-i18n';
+import { useAlert } from 'dashboard/composables';
+import { useStore } from 'dashboard/composables/store';
+import { useWhatsappEmbeddedSignup } from 'dashboard/composables/useWhatsappEmbeddedSignup';
+import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
+import googleClient from 'dashboard/api/channel/googleClient';
+import microsoftClient from 'dashboard/api/channel/microsoftClient';
+import instagramClient from 'dashboard/api/channel/instagramClient';
+import tiktokClient from 'dashboard/api/channel/tiktokClient';
+
+// Channels that complete via an OAuth redirect. Email channels are keyed by their
+// Channel::Email provider, others by channel type. The request is tagged with a
+// return hint so the callback brings the user back to onboarding instead of the
+// inbox settings page.
+const OAUTH_CLIENTS = {
+ google: googleClient,
+ microsoft: microsoftClient,
+ instagram: instagramClient,
+ tiktok: tiktokClient,
+};
+
+export function useChannelConnect() {
+ const { t } = useI18n();
+ const store = useStore();
+ const { runEmbeddedSignup } = useWhatsappEmbeddedSignup();
+
+ const connectViaOAuth = async provider => {
+ const client = OAUTH_CLIENTS[provider];
+ if (!client) return;
+
+ try {
+ const {
+ data: { url },
+ } = await client.generateAuthorization({ return_to: 'onboarding' });
+ window.location.href = url;
+ } catch {
+ useAlert(t('ONBOARDING_INBOX_SETUP.ERROR'));
+ }
+ };
+
+ // WhatsApp connects via Meta's embedded-signup popup instead of the redirect
+ // OAuth flow above. Collect the signup credentials, exchange them for an
+ // inbox, and surface the result inline — then refetch so the connected state
+ // reflects the freshly created inbox (and renders its real channel icon).
+ const connectWhatsapp = async () => {
+ let credentials;
+ try {
+ credentials = await runEmbeddedSignup();
+ } catch {
+ useAlert(t('ONBOARDING_INBOX_SETUP.ERROR'));
+ return;
+ }
+ if (!credentials) return; // user dismissed the popup
+
+ try {
+ await store.dispatch('inboxes/createWhatsAppEmbeddedSignup', credentials);
+ await store.dispatch('inboxes/get');
+ useAlert(t('ONBOARDING_INBOX_SETUP.WHATSAPP_CONNECTED'));
+ } catch (error) {
+ useAlert(
+ parseAPIErrorResponse(error) || t('ONBOARDING_INBOX_SETUP.ERROR')
+ );
+ }
+ };
+
+ return { connectViaOAuth, connectWhatsapp };
+}
diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useDetectedChannels.js b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useDetectedChannels.js
new file mode 100644
index 000000000..6ba68c6bf
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useDetectedChannels.js
@@ -0,0 +1,130 @@
+import { computed } from 'vue';
+import { useMapGetter } from 'dashboard/composables/store';
+import { useAccount } from 'dashboard/composables/useAccount';
+import {
+ SOCIAL_PLATFORMS,
+ EMAIL_PROVIDERS,
+ DEFAULT_CHANNEL_TYPES,
+} from './constants';
+import { findConnectedInbox } from './channelMatchers';
+import { useChannelConfig } from './useChannelConfig';
+
+// How many channel rows to show, whether detected or defaulted. DEFAULT_CHANNEL_TYPES
+// is config-gated like everything else, then sliced to this limit.
+const DISPLAYED_CHANNEL_LIMIT = 3;
+
+// Pull the handle/username out of a detected social URL, formatted per channel.
+const extractHandle = ({ type, url }) => {
+ try {
+ const { pathname } = new URL(url);
+ const path = pathname.replace(/^\/+|\/+$/g, '');
+ if (type === 'whatsapp') {
+ const digits = path.replace(/\D/g, '');
+ return digits ? `+${digits}` : '';
+ }
+ if (type === 'line') return path;
+ return path.startsWith('@') ? path : `@${path}`;
+ } catch {
+ return '';
+ }
+};
+
+// Derives the channel rows for the inbox-setup step from the account's detected
+// brand_info (socials + mailbox provider) and the real connected inboxes,
+// keeping InboxSetup.vue focused on layout, connect routing, and completion.
+export function useDetectedChannels() {
+ const { currentAccount } = useAccount();
+ const inboxes = useMapGetter('inboxes/getInboxes');
+ const { isConfigured } = useChannelConfig();
+
+ const brandSocials = computed(
+ () => currentAccount.value?.custom_attributes?.brand_info?.socials || []
+ );
+
+ const connectedChannels = computed(() =>
+ brandSocials.value
+ .filter(social => SOCIAL_PLATFORMS[social.type] && social.url)
+ .map(social => ({
+ type: social.type,
+ handle: extractHandle(social),
+ labelKey: SOCIAL_PLATFORMS[social.type].labelKey,
+ inbox: { channel_type: SOCIAL_PLATFORMS[social.type].channelType },
+ }))
+ );
+
+ const detectedEmailChannel = computed(() => {
+ const brandInfo = currentAccount.value?.custom_attributes?.brand_info;
+ const provider = brandInfo?.email_provider;
+ if (!EMAIL_PROVIDERS[provider]) return null;
+
+ return {
+ type: 'email',
+ handle: brandInfo?.email || '',
+ labelKey: EMAIL_PROVIDERS[provider].labelKey,
+ inbox: { channel_type: 'Channel::Email', provider },
+ };
+ });
+
+ // The real inbox backing a channel, if one exists — returned (not just a
+ // boolean) so the row can show the connected account's real name.
+ const connectedInbox = channel =>
+ findConnectedInbox(inboxes.value, channel.inbox);
+
+ // A channel row built from a social type, with no detected handle — used for
+ // the default suggestions when nothing was detected.
+ const toChannelRow = type => ({
+ type,
+ handle: '',
+ labelKey: SOCIAL_PLATFORMS[type].labelKey,
+ inbox: { channel_type: SOCIAL_PLATFORMS[type].channelType },
+ });
+
+ const detectedChannels = computed(() =>
+ [detectedEmailChannel.value, ...connectedChannels.value]
+ .filter(Boolean)
+ // Email channels (including Gmail/Outlook OAuth) are disabled for this
+ // phase; they will be enabled in a future PR.
+ .filter(channel => channel.type !== 'email')
+ // Hide channels whose installation OAuth credentials are missing — their
+ // connect flow would only error.
+ .filter(channel => isConfigured(channel.type))
+ );
+
+ const defaultChannels = computed(() =>
+ DEFAULT_CHANNEL_TYPES.filter(isConfigured)
+ .slice(0, DISPLAYED_CHANNEL_LIMIT)
+ .map(toChannelRow)
+ );
+
+ // Show the detected channels, or fall back to the default suggestions so the
+ // step is never an empty list.
+ const displayedChannels = computed(() =>
+ detectedChannels.value.length
+ ? detectedChannels.value
+ : defaultChannels.value
+ );
+
+ const remainingChannels = computed(() => {
+ // Exclude whatever is already shown as a row (detected or defaulted) so the
+ // footer preview doesn't duplicate it.
+ const shownTypes = new Set(displayedChannels.value.map(c => c.type));
+ return Object.entries(SOCIAL_PLATFORMS)
+ .filter(([type]) => !shownTypes.has(type))
+ .filter(([type]) => isConfigured(type))
+ .slice(0, 3)
+ .map(([type, { labelKey, channelType }]) => ({
+ type,
+ labelKey,
+ inbox: { channel_type: channelType },
+ }));
+ });
+
+ const hasDetectedChannels = computed(() => detectedChannels.value.length > 0);
+
+ return {
+ displayedChannels,
+ remainingChannels,
+ connectedInbox,
+ hasDetectedChannels,
+ };
+}
diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/OnboardingLayout.vue b/app/javascript/dashboard/routes/dashboard/onboarding/shared/OnboardingLayout.vue
similarity index 81%
rename from app/javascript/dashboard/routes/dashboard/onboarding/OnboardingLayout.vue
rename to app/javascript/dashboard/routes/dashboard/onboarding/shared/OnboardingLayout.vue
index 63b3fa391..90abff58f 100644
--- a/app/javascript/dashboard/routes/dashboard/onboarding/OnboardingLayout.vue
+++ b/app/javascript/dashboard/routes/dashboard/onboarding/shared/OnboardingLayout.vue
@@ -5,11 +5,12 @@ defineProps({
greeting: { type: String, required: true },
subtitle: { type: String, default: '' },
continueLabel: { type: String, default: 'Continue' },
+ skipLabel: { type: String, default: '' },
isLoading: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
});
-defineEmits(['continue']);
+defineEmits(['continue', 'skip']);
@@ -20,7 +21,7 @@ defineEmits(['continue']);
-
+
-
- {{ continueLabel }}
-
+
+
+ {{ continueLabel }}
+
+
+ {{ skipLabel }}
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/OnboardingSection.vue b/app/javascript/dashboard/routes/dashboard/onboarding/shared/OnboardingSection.vue
similarity index 87%
rename from app/javascript/dashboard/routes/dashboard/onboarding/OnboardingSection.vue
rename to app/javascript/dashboard/routes/dashboard/onboarding/shared/OnboardingSection.vue
index 479aa4c81..976e5fa9d 100644
--- a/app/javascript/dashboard/routes/dashboard/onboarding/OnboardingSection.vue
+++ b/app/javascript/dashboard/routes/dashboard/onboarding/shared/OnboardingSection.vue
@@ -4,6 +4,7 @@ import Icon from 'dashboard/components-next/icon/Icon.vue';
defineProps({
title: { type: String, required: true },
icon: { type: String, required: true },
+ bare: { type: Boolean, default: false },
});
@@ -42,8 +43,12 @@ defineProps({
-