From 47bb779afd4ae479395042d186ee987bb2a7e427 Mon Sep 17 00:00:00 2001 From: nfebe Date: Sat, 18 Jul 2026 23:02:45 +0100 Subject: [PATCH] feat(onboarding): Offer importing to get started, not only manual entry Onboarding walked someone through language, wallet and categories and then sent them to a blank dashboard, where the only way in was typing transactions one by one. Most people already have the data somewhere. A new step now offers to bring transactions in: import a file (a statement, a spreadsheet, or a receipt photo), or connect any account whose integration is installed and ready. Connectors are discovered from the integrations the server advertises, so a bank-sync plugin appears here on its own once wired, with no change to this step. Choosing one finishes setup and lands the person on that surface; anyone who would rather type it in can still skip straight through. --- assets/scss/_onboarding.scss | 60 +++++++++++++++++++++++ i18n/locales/de.json | 8 +++- i18n/locales/en.json | 8 +++- i18n/locales/es.json | 8 +++- i18n/locales/fr.json | 8 +++- pages/onboarding.vue | 92 +++++++++++++++++++++++++++++++++++- 6 files changed, 178 insertions(+), 6 deletions(-) diff --git a/assets/scss/_onboarding.scss b/assets/scss/_onboarding.scss index c386dc1..4e15ceb 100644 --- a/assets/scss/_onboarding.scss +++ b/assets/scss/_onboarding.scss @@ -635,3 +635,63 @@ margin: 0; padding-left: calc(20px + 0.75rem); } + +.import-options { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.import-option { + display: flex; + align-items: center; + gap: 1rem; + width: 100%; + padding: 1.25rem 1.5rem; + border: 2px solid $border-gray; + border-radius: 12px; + background: $bg-white; + cursor: pointer; + text-align: left; + transition: all 0.2s ease; + + &:hover { + border-color: $primary; + background: rgba(var(--color-primary-rgb), 0.05); + } +} + +.import-option-icon { + display: grid; + place-items: center; + width: 44px; + height: 44px; + border-radius: 10px; + background: rgba(var(--color-primary-rgb), 0.1); + color: $primary; + flex-shrink: 0; + + svg { + width: 24px; + height: 24px; + } +} + +.import-option-text { + display: flex; + flex-direction: column; + gap: 0.2rem; + min-width: 0; +} + +.import-option-title { + font-weight: 600; + font-size: 1rem; + color: $text-primary; +} + +.import-option-desc { + font-size: 0.875rem; + color: $text-secondary; + line-height: 1.4; +} diff --git a/i18n/locales/de.json b/i18n/locales/de.json index 40b5456..f01fc24 100644 --- a/i18n/locales/de.json +++ b/i18n/locales/de.json @@ -729,5 +729,11 @@ "Could not complete the actions": "Aktionen konnten nicht ausgeführt werden", "Could not dismiss the actions": "Aktionen konnten nicht verworfen werden", "{count} changes await your confirmation": "{count} Änderungen warten auf Ihre Bestätigung", - "{count} changes": "{count} Änderungen" + "{count} changes": "{count} Änderungen", + "Bring your transactions in": "Bring deine Transaktionen herein", + "Start with what you already have instead of typing it all in. You can always add more by hand later.": "Beginne mit dem, was du schon hast, statt alles einzutippen. Von Hand ergänzen kannst du später jederzeit.", + "Import from a file": "Aus einer Datei importieren", + "A statement, a spreadsheet, or a photo of a receipt.": "Ein Kontoauszug, eine Tabelle oder ein Foto eines Belegs.", + "Connect {name}": "{name} verbinden", + "I'll add transactions myself": "Ich füge die Transaktionen selbst hinzu" } diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 14a69cd..f1c2468 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -1346,5 +1346,11 @@ "Could not complete the actions": "Could not complete the actions", "Could not dismiss the actions": "Could not dismiss the actions", "{count} changes await your confirmation": "{count} changes await your confirmation", - "{count} changes": "{count} changes" + "{count} changes": "{count} changes", + "Bring your transactions in": "Bring your transactions in", + "Start with what you already have instead of typing it all in. You can always add more by hand later.": "Start with what you already have instead of typing it all in. You can always add more by hand later.", + "Import from a file": "Import from a file", + "A statement, a spreadsheet, or a photo of a receipt.": "A statement, a spreadsheet, or a photo of a receipt.", + "Connect {name}": "Connect {name}", + "I'll add transactions myself": "I'll add transactions myself" } diff --git a/i18n/locales/es.json b/i18n/locales/es.json index 44640ce..73662a9 100644 --- a/i18n/locales/es.json +++ b/i18n/locales/es.json @@ -729,5 +729,11 @@ "Could not complete the actions": "No se pudieron completar las acciones", "Could not dismiss the actions": "No se pudieron descartar las acciones", "{count} changes await your confirmation": "{count} cambios esperan tu confirmación", - "{count} changes": "{count} cambios" + "{count} changes": "{count} cambios", + "Bring your transactions in": "Trae tus transacciones", + "Start with what you already have instead of typing it all in. You can always add more by hand later.": "Empieza con lo que ya tienes en lugar de escribirlo todo. Siempre puedes añadir más a mano después.", + "Import from a file": "Importar desde un archivo", + "A statement, a spreadsheet, or a photo of a receipt.": "Un extracto, una hoja de cálculo o una foto de un recibo.", + "Connect {name}": "Conectar {name}", + "I'll add transactions myself": "Añadiré las transacciones yo mismo" } diff --git a/i18n/locales/fr.json b/i18n/locales/fr.json index abc78dd..e06a05b 100644 --- a/i18n/locales/fr.json +++ b/i18n/locales/fr.json @@ -729,5 +729,11 @@ "Could not complete the actions": "Impossible d'effectuer les actions", "Could not dismiss the actions": "Impossible d'ignorer les actions", "{count} changes await your confirmation": "{count} modifications attendent votre confirmation", - "{count} changes": "{count} modifications" + "{count} changes": "{count} modifications", + "Bring your transactions in": "Importez vos transactions", + "Start with what you already have instead of typing it all in. You can always add more by hand later.": "Partez de ce que vous avez déjà au lieu de tout saisir. Vous pourrez en ajouter à la main plus tard.", + "Import from a file": "Importer depuis un fichier", + "A statement, a spreadsheet, or a photo of a receipt.": "Un relevé, un tableur ou une photo de reçu.", + "Connect {name}": "Connecter {name}", + "I'll add transactions myself": "J'ajouterai les transactions moi-même" } diff --git a/pages/onboarding.vue b/pages/onboarding.vue index 418088b..a1ccd5e 100644 --- a/pages/onboarding.vue +++ b/pages/onboarding.vue @@ -248,6 +248,59 @@ + +
+
+ +
+
+

{{ $t('Bring your transactions in') }}

+

+ {{ + $t( + 'Start with what you already have instead of typing it all in. You can always add more by hand later.' + ) + }} +

+
+
+
+ + + +
+ +
+ +
+
+
+
@@ -322,10 +375,14 @@ import { import IconLanguage from '~icons/solar/translation-bold-duotone'; import IconWallet from '~icons/solar/wallet-money-bold-duotone'; import IconCategory from '~icons/solar/widget-5-bold-duotone'; +import IconImport from '~icons/solar/import-bold-duotone'; +import IconFileUpload from '~icons/solar/cloud-upload-bold-duotone'; +import IconConnect from '~icons/solar/link-bold-duotone'; import { useSharedData } from '@/composables/useSharedData'; import { useNotifications } from '@/composables/useNotifications'; import { useWallets } from '@/composables/useWallets'; import { useExtensionSlots } from '@/composables/useExtensionSlots'; +import { useIntegrations } from '@/composables/useIntegrations'; import DescriptorRenderer from '@/components/extensions/DescriptorRenderer.vue'; import configurationsApi from '@/services/api/configurationsApi'; import { CONFIGURATION_KEYS } from '@/utils/configurationKeys'; @@ -347,6 +404,36 @@ const { createWallet, updateWallet } = useWallets(); const { setComplete: setOnboardingComplete } = useOnboardingStatus(); const returnTo = computed(() => (route.query.returnTo as string) || '/dashboard'); +// Where onboarding lands when it finishes. Defaults to returnTo, but the import +// step overrides it so "import a file" or "connect an account" drop the user +// straight onto that surface instead of the dashboard. +const pendingDestination = ref(null); +const finalDestination = computed(() => pendingDestination.value ?? returnTo.value); + +const { integrations, load: loadIntegrations } = useIntegrations(); +onMounted(() => { + loadIntegrations(); +}); + +// Connectors that can pull transactions in from a linked account (a bank, etc). +// Only ready-to-use ones: entitled, configured, and declaring a connect flow. +// File import is offered separately as the always-present built-in option. +const connectors = computed(() => + integrations.value.filter( + (integration) => integration.entitled && integration.configured && integration.ui?.connect + ) +); + +const startFileImport = () => { + pendingDestination.value = '/imports'; + completeOnboarding(); +}; + +const startConnector = (integration: (typeof integrations.value)[number]) => { + pendingDestination.value = integration.ui?.onboarding?.href ?? '/settings'; + completeOnboarding(); +}; + const currentStep = ref(1); // Onboarding is a data-driven list: built-in steps plus any plugin @@ -359,6 +446,7 @@ const builtInSteps = [ { id: 'language', order: 10 }, { id: 'wallet', order: 20 }, { id: 'categories', order: 30 }, + { id: 'import', order: 40 }, { id: 'complete', order: 1000 } ]; @@ -465,7 +553,7 @@ const ensureOnboardingComplete = async () => { const skipStep = async () => { try { await ensureOnboardingComplete(); - await navigateTo(returnTo.value, { replace: true }); + await navigateTo(finalDestination.value, { replace: true }); } catch (e) { console.error('Error in skipStep:', e); } @@ -641,7 +729,7 @@ const completeOnboarding = async () => { sharedData.loadConfigurations(true).catch(() => {}), sharedData.loadWallets(true).catch(() => {}) ]); - await navigateTo(returnTo.value, { replace: true }); + await navigateTo(finalDestination.value, { replace: true }); } finally { isCompleting.value = false; }