From 0c46fe33d2d6b160d72ccb772d290996f38e00d6 Mon Sep 17 00:00:00 2001 From: MindCollaps Date: Thu, 22 Jan 2026 01:53:48 +0100 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=95=B5=EF=B8=8F=20More=20immoscout=20?= =?UTF-8?q?details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added more details to immoscout api - description is now populated with a lot of data from the expose using app API - You can ignore certificates, if deploying locally and using the http notification adapter - More details for the test call/example for easier testing + placeholder image + actual values + address (famous Erika Mustermans address see https://de.wikipedia.org/wiki/Mustermann) - Grater timeout for geocode since the api is sometimes slow in germany - uiElement, type boolean, now has a label as well --- lib/api/routes/notificationAdapterRouter.js | 62 +++++++++++++-- lib/notification/adapter/http.js | 24 +++++- lib/provider/immoscout.js | 75 ++++++++++++++----- .../geocoding/client/nominatimClient.js | 1 + .../NotificationAdapterMutator.jsx | 5 +- 5 files changed, 140 insertions(+), 27 deletions(-) diff --git a/lib/api/routes/notificationAdapterRouter.js b/lib/api/routes/notificationAdapterRouter.js index fd34cc6f..e7abf8c7 100644 --- a/lib/api/routes/notificationAdapterRouter.js +++ b/lib/api/routes/notificationAdapterRouter.js @@ -34,11 +34,14 @@ notificationAdapterRouter.post('/try', async (req, res) => { serviceName: 'TestCall', newListings: [ { - price: '42 €', - title: 'This is a test listing', - address: 'some address', - size: '666 2m', - link: 'https://www.orange-coding.net', + address: 'Heidestrasse 17, 51147 Köln', + description: exampleDescription, + id: '1', + imageUrl: 'https://placehold.co/600x400/png', + price: '1.000 €', + size: '76 m²', + title: 'Stilvolle gepflegte 3-Raum-Wohnung mit gehobener Innenausstattung', + url: 'https://www.orange-coding.net', }, ], notificationConfig, @@ -46,6 +49,7 @@ notificationAdapterRouter.post('/try', async (req, res) => { }); res.send(); } catch (Exception) { + console.error('Error during notification adapter test:', Exception); res.send(new Error(Exception)); } }); @@ -54,3 +58,51 @@ notificationAdapterRouter.get('/', async (req, res) => { res.send(); }); export { notificationAdapterRouter }; + +const exampleDescription = ` +Wohnungstyp: Etagenwohnung +Nutzfläche: 76 m² +Etage: 2 von 3 +Schlafzimmer: 1 +Badezimmer: 1 +Bezugsfrei ab: 1.4.2026 +Haustiere: Nein +Garage/Stellplatz: Tiefgarage +Anzahl Garage/Stellplatz: 1 +Kaltmiete (zzgl. Nebenkosten): 1.000 € +Preis/m²: 13,16 €/m² +Nebenkosten: 230 € +Heizkosten in Nebenkosten enthalten: Ja +Gesamtmiete: 1.230 € +Kaution: 3.000,00 +Preis pro Parkfläche: 60 € +Baujahr: 2000 +Objektzustand: Modernisiert +Qualität der Ausstattung: Gehoben +Heizungsart: Fernwärme +Energieausweistyp: Verbrauchsausweis +Energieausweis: liegt vor +Endenergieverbrauch: 72 kWh/(m²∙a) +Baujahr laut Energieausweis: 2000 + +Diese moderne 3-Zimmer-Wohnung liegt direkt neben einem Park und nur wenige Minuten von der S-Bahn-Haltestelle entfernt. Das Stadtzentrum sowie Freizeiteinrichtungen sind 1,5 km entfernt. + +Die Wohnung ist ideal für Paare oder kleine Familien geeignet. + +Ausstattung: +- neuer hochwertiger Bodenbelag (Holzoptik) in allen Räumen außer Bad/Küche +- sonniger Balkon (Süd) +- Tiefgaragenstellplatz +- Kellerabteil +- gepflegtes Mehrfamilienhaus + +Die Küche ist vom Mieter nach eigenen Wünschen einzurichten. + +Vermietung direkt vom Eigentümer - provisionsfrei! + +Lage: +• Park: 1 Minute zu Fuß +• S-Bahn Station: 2 Minuten zu Fuß +• Supermärkte, Restaurants, täglicher Bedarf in der Nähe +• Gute Anbindung Richtung Großstadt und Flughafen +`; diff --git a/lib/notification/adapter/http.js b/lib/notification/adapter/http.js index 00e23840..fd691b28 100644 --- a/lib/notification/adapter/http.js +++ b/lib/notification/adapter/http.js @@ -4,6 +4,7 @@ */ import { markdown2Html } from '../../services/markdown.js'; +import { Agent, fetch } from 'undici'; const mapListing = (listing) => ({ address: listing.address, @@ -17,7 +18,7 @@ const mapListing = (listing) => ({ }); export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => { - const { authToken, endpointUrl } = notificationConfig.find((a) => a.id === config.id).fields; + const { authToken, endpointUrl, selfSignedCerts } = notificationConfig.find((a) => a.id === config.id).fields; const listings = newListings.map(mapListing); const body = { @@ -34,11 +35,22 @@ export const send = ({ serviceName, newListings, notificationConfig, jobKey }) = headers['Authorization'] = `Bearer ${authToken}`; } - return fetch(endpointUrl, { + let fetchOptions = { method: 'POST', - headers: headers, + headers, + timeout: 10000, body: JSON.stringify(body), - }); + }; + + if (selfSignedCerts === true) { + fetchOptions.dispatcher = new Agent({ + connect: { + rejectUnauthorized: false, + }, + }); + } + + return fetch(endpointUrl, fetchOptions); }; export const config = { @@ -52,6 +64,10 @@ export const config = { label: 'Endpoint URL', type: 'text', }, + selfSignedCerts: { + label: 'Self-signed certificates', + type: 'boolean', + }, authToken: { description: "Your application's auth token, if required by your endpoint.", label: 'Auth token (optional)', diff --git a/lib/provider/immoscout.js b/lib/provider/immoscout.js index 6047f006..fefd3d54 100644 --- a/lib/provider/immoscout.js +++ b/lib/provider/immoscout.js @@ -66,23 +66,63 @@ async function getListings(url) { } const responseBody = await response.json(); - return responseBody.resultListItems - .filter((item) => item.type === 'EXPOSE_RESULT') - .map((expose) => { - const item = expose.item; - const [price, size] = item.attributes; - const image = item?.titlePicture?.preview ?? null; - return { - id: item.id, - price: price?.value, - size: size?.value, - title: item.title, - description: item.description, - link: `${metaInformation.baseUrl}expose/${item.id}`, - address: item.address?.line, - image, - }; - }); + return Promise.all( + responseBody.resultListItems + .filter((item) => item.type === 'EXPOSE_RESULT') + .map(async (expose) => { + const item = expose.item; + const [price, size] = item.attributes; + const image = item?.titlePicture?.full ?? item?.titlePicture?.preview ?? null; + let listing = { + id: item.id, + price: price?.value, + size: size?.value, + title: item.title, + link: `${metaInformation.baseUrl}expose/${item.id}`, + address: item.address?.line, + image, + }; + return await pushDetails(listing); + }), + ); +} + +async function pushDetails(listing) { + const detailed = await fetch(`https://api.mobile.immobilienscout24.de/expose/${listing.id}`, { + headers: { + 'User-Agent': 'ImmoScout_27.3_26.0_._', + 'Content-Type': 'application/json', + }, + }); + if (!detailed.ok) { + logger.error('Error fetching listing details from ImmoScout Mobile API:', detailed.statusText); + return ''; + } + const detailBody = await detailed.json(); + + listing.description = buildDescription(detailBody); + + return listing; +} + +function buildDescription(detailBody) { + const sections = detailBody.sections || []; + + const attributes = sections + .filter((s) => s.type === 'ATTRIBUTE_LIST') + .flatMap((s) => s.attributes) + .filter((attr) => attr.label && attr.text) + .map((attr) => `${attr.label} ${attr.text}`) + .join('\n'); + + const freeText = sections + .filter((s) => s.type === 'TEXT_AREA') + .map((s) => { + return `${s.title}\n${s.text}`; + }) + .join('\n\n'); + + return attributes.trim() + '\n\n' + freeText.trim(); } async function isListingActive(link) { @@ -125,6 +165,7 @@ const config = { size: 'size', link: 'link', address: 'address', + description: 'description', }, // Not required - used by filter to remove and listings that failed to parse sortByDateParam: 'sorting=-firstactivation', diff --git a/lib/services/geocoding/client/nominatimClient.js b/lib/services/geocoding/client/nominatimClient.js index fdc70d34..444b0e5f 100644 --- a/lib/services/geocoding/client/nominatimClient.js +++ b/lib/services/geocoding/client/nominatimClient.js @@ -67,6 +67,7 @@ async function doGeocode(address) { try { const response = await fetch(url, { agent, + timeout: 60000, headers: { 'User-Agent': userAgent, }, diff --git a/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx b/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx index e5b75a02..7b93adba 100644 --- a/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx +++ b/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx @@ -153,12 +153,15 @@ export default function NotificationAdapterMutator({ return (
{uiElement.type === 'boolean' ? ( - + { setValue(selectedAdapter, uiElement, key, checked); }} /> + {uiElement.label} + ) : ( Date: Mon, 26 Jan 2026 01:42:49 +0100 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=91=80=20Requested=20changes=20+=20so?= =?UTF-8?q?me=20extra?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Req: - using logger - using node-fetch Extra: - boolean input fields will trigger the validate check, because they are set undefined at first - setting them to false if they are undefined now - added more data to the description (phone number and name of the agent) --- lib/api/routes/notificationAdapterRouter.js | 4 +++- lib/notification/adapter/http.js | 8 +++----- lib/provider/immoscout.js | 14 +++++++++++++- .../NotificationAdapterMutator.jsx | 5 ++++- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/api/routes/notificationAdapterRouter.js b/lib/api/routes/notificationAdapterRouter.js index e7abf8c7..789aee4d 100644 --- a/lib/api/routes/notificationAdapterRouter.js +++ b/lib/api/routes/notificationAdapterRouter.js @@ -5,6 +5,8 @@ import fs from 'fs'; import restana from 'restana'; +import logger from './lib/services/logger.js'; + const service = restana(); const notificationAdapterRouter = service.newRouter(); const notificationAdapterList = fs.readdirSync('./lib//notification/adapter').filter((file) => file.endsWith('.js')); @@ -49,7 +51,7 @@ notificationAdapterRouter.post('/try', async (req, res) => { }); res.send(); } catch (Exception) { - console.error('Error during notification adapter test:', Exception); + logger.error('Error during notification adapter test:', Exception); res.send(new Error(Exception)); } }); diff --git a/lib/notification/adapter/http.js b/lib/notification/adapter/http.js index fd691b28..1376340d 100644 --- a/lib/notification/adapter/http.js +++ b/lib/notification/adapter/http.js @@ -4,7 +4,7 @@ */ import { markdown2Html } from '../../services/markdown.js'; -import { Agent, fetch } from 'undici'; +import https from 'node:https'; const mapListing = (listing) => ({ address: listing.address, @@ -43,10 +43,8 @@ export const send = ({ serviceName, newListings, notificationConfig, jobKey }) = }; if (selfSignedCerts === true) { - fetchOptions.dispatcher = new Agent({ - connect: { - rejectUnauthorized: false, - }, + fetchOptions.agent = new https.Agent({ + rejectUnauthorized: false, }); } diff --git a/lib/provider/immoscout.js b/lib/provider/immoscout.js index fefd3d54..60bf12f3 100644 --- a/lib/provider/immoscout.js +++ b/lib/provider/immoscout.js @@ -107,6 +107,15 @@ async function pushDetails(listing) { function buildDescription(detailBody) { const sections = detailBody.sections || []; + const contact = detailBody.contact || {}; + const cData = contact?.contactData || {}; + const agentName = cData?.agent?.name || ''; + const agentCompany = cData?.agent?.company || ''; + const stars = cData?.agent?.rating?.numberOfStars || ''; + const phoneNumbers = contact?.phoneNumbers || []; + const phoneNumbersMapped = phoneNumbers + .map((p) => `${p.label}: ${p.text}`) + .join('\n').trim(); const attributes = sections .filter((s) => s.type === 'ATTRIBUTE_LIST') @@ -122,7 +131,10 @@ function buildDescription(detailBody) { }) .join('\n\n'); - return attributes.trim() + '\n\n' + freeText.trim(); + return `Agent: ${agentName ? agentName : 'Unbekannt'} ${agentCompany ? `(${agentCompany}) ` : ''}${stars ? `- ${stars} stars` : ''}\n` + + (phoneNumbersMapped ? `Phone Numbers:\n${phoneNumbersMapped}` : '') + '\n\n' + + attributes.trim() + '\n\n' + + freeText.trim(); } async function isListingActive(link) { diff --git a/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx b/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx index b8e5cd58..6e0e5963 100644 --- a/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx +++ b/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx @@ -27,10 +27,13 @@ const sortAdapter = (a, b) => { const validate = (selectedAdapter) => { const results = []; for (let uiElement of Object.values(selectedAdapter.fields || [])) { - if (uiElement.value == null && !uiElement.optional) { + if (uiElement.value == null && !uiElement.optional && uiElement.type !== 'boolean') { results.push('All fields are mandatory and must be set.'); continue; } + if (uiElement.type === 'boolean' && typeof uiElement.value !== 'boolean') { + uiElement.value = false; + } if (uiElement.type === 'number') { const numberValue = parseFloat(uiElement.value); if (isNaN(numberValue) || numberValue < 0) { From 09a95a6fe41bb04477a92609653d35ada02605fa Mon Sep 17 00:00:00 2001 From: MindCollaps Date: Mon, 26 Jan 2026 10:51:15 +0100 Subject: [PATCH 3/5] =?UTF-8?q?=E2=9C=85=20Fixed=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/api/routes/notificationAdapterRouter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/routes/notificationAdapterRouter.js b/lib/api/routes/notificationAdapterRouter.js index 789aee4d..eeb5e68b 100644 --- a/lib/api/routes/notificationAdapterRouter.js +++ b/lib/api/routes/notificationAdapterRouter.js @@ -5,7 +5,7 @@ import fs from 'fs'; import restana from 'restana'; -import logger from './lib/services/logger.js'; +import logger from '../../services/logger.js'; const service = restana(); const notificationAdapterRouter = service.newRouter(); From eb1d3741e500e64827d4adf50b104e7995029f4e Mon Sep 17 00:00:00 2001 From: MindCollaps Date: Sun, 1 Mar 2026 01:40:39 +0100 Subject: [PATCH 4/5] =?UTF-8?q?=E2=9C=85=EF=B8=8F=20Toggle=20immoscout=20d?= =?UTF-8?q?etail=20fetching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/api/routes/userSettingsRoute.js | 21 +++++++++++++ lib/notification/adapter/http.js | 7 ++--- lib/provider/immoscout.js | 34 +++++++++++++++++----- lib/services/jobs/jobExecutionService.js | 7 +++++ ui/src/services/state/store.js | 14 +++++++++ ui/src/views/userSettings/UserSettings.jsx | 32 ++++++++++++++++++-- 6 files changed, 101 insertions(+), 14 deletions(-) diff --git a/lib/api/routes/userSettingsRoute.js b/lib/api/routes/userSettingsRoute.js index 9b34b83e..78631266 100644 --- a/lib/api/routes/userSettingsRoute.js +++ b/lib/api/routes/userSettingsRoute.js @@ -97,4 +97,25 @@ userSettingsRouter.post('/news-hash', async (req, res) => { } }); +userSettingsRouter.post('/immoscout-details', async (req, res) => { + const userId = req.session.currentUser; + const { immoscout_details } = req.body; + + const globalSettings = await getSettings(); + if (globalSettings.demoMode) { + res.statusCode = 403; + res.send({ error: 'In demo mode, it is not allowed to change settings.' }); + return; + } + + try { + upsertSettings({ immoscout_details: !!immoscout_details }, userId); + res.send({ success: true }); + } catch (error) { + logger.error('Error updating immoscout details setting', error); + res.statusCode = 500; + res.send({ error: error.message }); + } +}); + export { userSettingsRouter }; diff --git a/lib/notification/adapter/http.js b/lib/notification/adapter/http.js index 1376340d..7f1c6f3a 100644 --- a/lib/notification/adapter/http.js +++ b/lib/notification/adapter/http.js @@ -4,7 +4,6 @@ */ import { markdown2Html } from '../../services/markdown.js'; -import https from 'node:https'; const mapListing = (listing) => ({ address: listing.address, @@ -17,7 +16,7 @@ const mapListing = (listing) => ({ url: listing.link, }); -export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => { +export const send = async ({ serviceName, newListings, notificationConfig, jobKey }) => { const { authToken, endpointUrl, selfSignedCerts } = notificationConfig.find((a) => a.id === config.id).fields; const listings = newListings.map(mapListing); @@ -43,8 +42,8 @@ export const send = ({ serviceName, newListings, notificationConfig, jobKey }) = }; if (selfSignedCerts === true) { - fetchOptions.agent = new https.Agent({ - rejectUnauthorized: false, + fetchOptions.dispatcher = new (await import('undici')).Agent({ + connect: { rejectUnauthorized: false }, }); } diff --git a/lib/provider/immoscout.js b/lib/provider/immoscout.js index 42b280c1..c12a54ce 100644 --- a/lib/provider/immoscout.js +++ b/lib/provider/immoscout.js @@ -47,6 +47,7 @@ import { } from '../services/immoscout/immoscout-web-translator.js'; import logger from '../services/logger.js'; let appliedBlackList = []; +let fetchDetails = false; async function getListings(url) { const response = await fetch(url, { @@ -82,7 +83,10 @@ async function getListings(url) { address: item.address?.line, image, }; - return await pushDetails(listing); + if (fetchDetails) { + return await pushDetails(listing); + } + return listing; }), ); } @@ -96,7 +100,7 @@ async function pushDetails(listing) { }); if (!detailed.ok) { logger.error('Error fetching listing details from ImmoScout Mobile API:', detailed.statusText); - return ''; + return listing; } const detailBody = await detailed.json(); @@ -115,7 +119,8 @@ function buildDescription(detailBody) { const phoneNumbers = contact?.phoneNumbers || []; const phoneNumbersMapped = phoneNumbers .map((p) => `${p.label}: ${p.text}`) - .join('\n').trim(); + .join('\n') + .trim(); const attributes = sections .filter((s) => s.type === 'ATTRIBUTE_LIST') @@ -131,10 +136,14 @@ function buildDescription(detailBody) { }) .join('\n\n'); - return `Agent: ${agentName ? agentName : 'Unbekannt'} ${agentCompany ? `(${agentCompany}) ` : ''}${stars ? `- ${stars} stars` : ''}\n` + - (phoneNumbersMapped ? `Phone Numbers:\n${phoneNumbersMapped}` : '') + '\n\n' + - attributes.trim() + '\n\n' + - freeText.trim(); + return ( + `Agent: ${agentName ? agentName : 'Unbekannt'} ${agentCompany ? `(${agentCompany}) ` : ''}${stars ? `- ${stars} stars` : ''}\n` + + (phoneNumbersMapped ? `Phone Numbers:\n${phoneNumbersMapped}` : '') + + '\n\n' + + attributes.trim() + + '\n\n' + + freeText.trim() + ); } async function isListingActive(link) { @@ -177,7 +186,6 @@ const config = { size: 'size', link: 'link', address: 'address', - description: 'description', }, // Not required - used by filter to remove and listings that failed to parse sortByDateParam: 'sorting=-firstactivation', @@ -190,6 +198,16 @@ export const init = (sourceConfig, blacklist) => { config.enabled = sourceConfig.enabled; config.url = convertWebToMobile(sourceConfig.url); appliedBlackList = blacklist || []; + fetchDetails = false; +}; + +/** + * Enable or disable fetching detailed listing data from the expose API. + * This creates additional API requests per listing and increases the risk of being rate-limited. + * @param {boolean} enabled + */ +export const setFetchDetails = (enabled) => { + fetchDetails = !!enabled; }; export const metaInformation = { name: 'Immoscout', diff --git a/lib/services/jobs/jobExecutionService.js b/lib/services/jobs/jobExecutionService.js index b52702d9..e0d425ad 100644 --- a/lib/services/jobs/jobExecutionService.js +++ b/lib/services/jobs/jobExecutionService.js @@ -14,6 +14,7 @@ import * as similarityCache from '../similarity-check/similarityCache.js'; import { isRunning, markFinished, markRunning } from './run-state.js'; import { sendToUsers } from '../sse/sse-broker.js'; import * as puppeteerExtractor from '../extractor/puppeteerExtractor.js'; +import { getUserSettings } from '../storage/settingsStorage.js'; /** * Initializes the job execution service. @@ -168,6 +169,12 @@ export function initJobExecutionService({ providers, settings, intervalMs }) { const matchedProvider = providers.find((loaded) => loaded.metaInformation.id === prov.id); matchedProvider.init(prov, job.blacklist); + // If the provider supports detail fetching (e.g. immoscout), check user setting + if (typeof matchedProvider.setFetchDetails === 'function' && job.userId) { + const userSettings = getUserSettings(job.userId); + matchedProvider.setFetchDetails(!!userSettings.immoscout_details); + } + if (browser && !browser.isConnected()) { logger.debug('Browser is disconnected, nullifying to launch a new one.'); await puppeteerExtractor.closeBrowser(browser); diff --git a/ui/src/services/state/store.js b/ui/src/services/state/store.js index 2fe44ba9..d76f1dd4 100644 --- a/ui/src/services/state/store.js +++ b/ui/src/services/state/store.js @@ -304,6 +304,20 @@ export const useFredyState = create( throw Exception; } }, + async setImmoscoutDetails(enabled) { + try { + await xhrPost('/api/user/settings/immoscout-details', { immoscout_details: enabled }); + set((state) => ({ + userSettings: { + ...state.userSettings, + settings: { ...state.userSettings.settings, immoscout_details: enabled }, + }, + })); + } catch (Exception) { + console.error('Error while trying to update immoscout details setting. Error:', Exception); + throw Exception; + } + }, }, }; diff --git a/ui/src/views/userSettings/UserSettings.jsx b/ui/src/views/userSettings/UserSettings.jsx index 939a7829..3373374a 100644 --- a/ui/src/views/userSettings/UserSettings.jsx +++ b/ui/src/views/userSettings/UserSettings.jsx @@ -4,8 +4,8 @@ */ import { useEffect, useState, useMemo } from 'react'; -import { Divider, Button, AutoComplete, Toast, Banner } from '@douyinfe/semi-ui-19'; -import { IconSave, IconHome } from '@douyinfe/semi-icons'; +import { Divider, Button, AutoComplete, Toast, Banner, Switch } from '@douyinfe/semi-ui-19'; +import { IconSave, IconHome, IconSearch } from '@douyinfe/semi-icons'; import { useSelector, useActions, useIsLoading } from '../../services/state/store'; import { xhrGet } from '../../services/xhr'; import { SegmentPart } from '../../components/segment/SegmentPart'; @@ -14,6 +14,7 @@ import debounce from 'lodash/debounce'; const UserSettings = () => { const actions = useActions(); const homeAddress = useSelector((state) => state.userSettings.settings.home_address); + const immoscoutDetails = useSelector((state) => state.userSettings.settings.immoscout_details); const [address, setAddress] = useState(homeAddress?.address || ''); const [coords, setCoords] = useState(homeAddress?.coords || null); const saving = useIsLoading(actions.userSettings.setHomeAddress); @@ -84,6 +85,33 @@ const UserSettings = () => { + + +
+ { + try { + await actions.userSettings.setImmoscoutDetails(checked); + Toast.success('ImmoScout details setting updated.'); + } catch { + Toast.error('Failed to update setting.'); + } + }} + /> + Fetch detailed ImmoScout listings +
+
+