Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions assets/scss/_onboarding.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
8 changes: 7 additions & 1 deletion i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
8 changes: 7 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
8 changes: 7 additions & 1 deletion i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
8 changes: 7 additions & 1 deletion i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
92 changes: 90 additions & 2 deletions pages/onboarding.vue
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,59 @@
</div>
</div>

<!-- Step: bring transactions in (file import + any connectors) -->
<div v-if="currentStepId === 'import'" class="step-content">
<div class="step-icon">
<IconImport />
</div>
<div class="step-info">
<h2 class="step-title">{{ $t('Bring your transactions in') }}</h2>
<p class="step-description">
{{
$t(
'Start with what you already have instead of typing it all in. You can always add more by hand later.'
)
}}
</p>
</div>
<div class="step-form">
<div class="import-options">
<button class="import-option" @click="startFileImport">
<span class="import-option-icon"><IconFileUpload /></span>
<span class="import-option-text">
<span class="import-option-title">{{ $t('Import from a file') }}</span>
<span class="import-option-desc">{{
$t('A statement, a spreadsheet, or a photo of a receipt.')
}}</span>
</span>
</button>

<button
v-for="connector in connectors"
:key="connector.key"
class="import-option"
@click="startConnector(connector)"
>
<span class="import-option-icon"><IconConnect /></span>
<span class="import-option-text">
<span class="import-option-title">{{
connector.ui?.card?.cta || $t('Connect {name}', { name: connector.name })
}}</span>
<span class="import-option-desc">{{
connector.ui?.card?.description || connector.description
}}</span>
</span>
</button>
</div>

<div class="step-actions">
<button class="secondary-btn" @click="nextStep">
{{ $t("I'll add transactions myself") }}
</button>
</div>
</div>
</div>

<!-- Step: plugin-contributed onboarding step (rendered from descriptor) -->
<div v-else-if="currentStepContribution" class="step-content">
<DescriptorRenderer :contribution="currentStepContribution" @next="nextStep" />
Expand Down Expand Up @@ -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';
Expand All @@ -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<string | null>(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
Expand All @@ -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 }
];

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
}
Expand Down
Loading