@bytebrand/fe-ui-core-autobahn 1.0.80 → 1.0.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,62 +1,62 @@
1
- // Autobahn (auto.de redesign) — shared financing-number formatting.
2
- //
3
- // Extracted from vdp/VdpModules.tsx's FinanceDetailsModal so non-React consumers (e.g. the
4
- // Meta AIA marketing-image renderer) format the SAME real numbers the same way, instead of a
5
- // second copy drifting from the VDP's. Pure TS, no React/JSX — safe to import from a plain
6
- // Node service.
7
- //
8
- // The "only real figures, never a fabricated total" rule is load-bearing (legal: balloon
9
- // financing means monthly×term is NOT the total) and lives here once, not per-caller.
10
-
11
- /** Monetary figure → de-DE grouped string (e.g. 47400 → "47.400"). Feed rounded integers —
12
- * toLocaleString on a fractional value prints a bogus 3-decimal grouping. */
13
- export function formatEur(n: number): string {
14
- return n.toLocaleString('de-DE');
15
- }
16
-
17
- /** Rate → de-DE percent string (e.g. 6.49 → "6,49 %"). */
18
- export function formatPct(n: number): string {
19
- return `${n.toFixed(2).replace('.', ',')} %`;
20
- }
21
-
22
- export interface FinanceExampleFields {
23
- price: number;
24
- apr: number;
25
- down: number;
26
- /** Real config figure — falls back to price-down ONLY when genuinely absent. */
27
- netLoanAmount?: number;
28
- nominalRate?: number;
29
- finalInstallment?: number;
30
- grossAmount?: number;
31
- }
32
-
33
- export interface FinanceExampleLabels {
34
- vehiclePrice: string;
35
- priceSuffix: string;
36
- apr: string;
37
- nominal: string;
38
- net: string;
39
- down: string;
40
- final: string;
41
- gross: string;
42
- }
43
-
44
- /**
45
- * The "Repräsentatives Beispiel" disclaimer line (PAngV). Only fields with a genuine value are
46
- * included — nominalRate/finalInstallment/grossAmount are optional real config figures, never
47
- * derived (see fields doc). Matches production wording/format exactly (VdpModules.tsx's
48
- * `exampleLine`, byte-for-byte).
49
- */
50
- export function buildFinanceExampleLine(fields: FinanceExampleFields, labels: FinanceExampleLabels, footnoteMarker = '³'): string {
51
- const netto = fields.netLoanAmount != null ? fields.netLoanAmount : Math.max(0, fields.price - fields.down);
52
- const parts: string[] = [
53
- `${labels.vehiclePrice}: ${formatEur(fields.price)} € ${labels.priceSuffix}`,
54
- ...(fields.apr > 0 ? [`${labels.apr}: ${formatPct(fields.apr)}`] : []),
55
- ...(fields.nominalRate != null && fields.nominalRate > 0 ? [`${labels.nominal}: ${formatPct(fields.nominalRate)}`] : []),
56
- `${labels.net}: ${formatEur(netto)} €`,
57
- `${labels.down}: ${formatEur(fields.down)} €`,
58
- ...(fields.finalInstallment != null && fields.finalInstallment > 0 ? [`${labels.final}: ${formatEur(fields.finalInstallment)} €`] : []),
59
- ...(fields.grossAmount != null && fields.grossAmount > 0 ? [`${labels.gross}: ${formatEur(fields.grossAmount)} €`] : []),
60
- ];
61
- return `${footnoteMarker} ${parts.join('; ')};`;
62
- }
1
+ // Autobahn (auto.de redesign) — shared financing-number formatting.
2
+ //
3
+ // Extracted from vdp/VdpModules.tsx's FinanceDetailsModal so non-React consumers (e.g. the
4
+ // Meta AIA marketing-image renderer) format the SAME real numbers the same way, instead of a
5
+ // second copy drifting from the VDP's. Pure TS, no React/JSX — safe to import from a plain
6
+ // Node service.
7
+ //
8
+ // The "only real figures, never a fabricated total" rule is load-bearing (legal: balloon
9
+ // financing means monthly×term is NOT the total) and lives here once, not per-caller.
10
+
11
+ /** Monetary figure → de-DE grouped string (e.g. 47400 → "47.400"). Feed rounded integers —
12
+ * toLocaleString on a fractional value prints a bogus 3-decimal grouping. */
13
+ export function formatEur(n: number): string {
14
+ return n.toLocaleString('de-DE');
15
+ }
16
+
17
+ /** Rate → de-DE percent string (e.g. 6.49 → "6,49 %"). */
18
+ export function formatPct(n: number): string {
19
+ return `${n.toFixed(2).replace('.', ',')} %`;
20
+ }
21
+
22
+ export interface FinanceExampleFields {
23
+ price: number;
24
+ apr: number;
25
+ down: number;
26
+ /** Real config figure — falls back to price-down ONLY when genuinely absent. */
27
+ netLoanAmount?: number;
28
+ nominalRate?: number;
29
+ finalInstallment?: number;
30
+ grossAmount?: number;
31
+ }
32
+
33
+ export interface FinanceExampleLabels {
34
+ vehiclePrice: string;
35
+ priceSuffix: string;
36
+ apr: string;
37
+ nominal: string;
38
+ net: string;
39
+ down: string;
40
+ final: string;
41
+ gross: string;
42
+ }
43
+
44
+ /**
45
+ * The "Repräsentatives Beispiel" disclaimer line (PAngV). Only fields with a genuine value are
46
+ * included — nominalRate/finalInstallment/grossAmount are optional real config figures, never
47
+ * derived (see fields doc). Matches production wording/format exactly (VdpModules.tsx's
48
+ * `exampleLine`, byte-for-byte).
49
+ */
50
+ export function buildFinanceExampleLine(fields: FinanceExampleFields, labels: FinanceExampleLabels, footnoteMarker = '³'): string {
51
+ const netto = fields.netLoanAmount != null ? fields.netLoanAmount : Math.max(0, fields.price - fields.down);
52
+ const parts: string[] = [
53
+ `${labels.vehiclePrice}: ${formatEur(fields.price)} € ${labels.priceSuffix}`,
54
+ ...(fields.apr > 0 ? [`${labels.apr}: ${formatPct(fields.apr)}`] : []),
55
+ ...(fields.nominalRate != null && fields.nominalRate > 0 ? [`${labels.nominal}: ${formatPct(fields.nominalRate)}`] : []),
56
+ `${labels.net}: ${formatEur(netto)} €`,
57
+ `${labels.down}: ${formatEur(fields.down)} €`,
58
+ ...(fields.finalInstallment != null && fields.finalInstallment > 0 ? [`${labels.final}: ${formatEur(fields.finalInstallment)} €`] : []),
59
+ ...(fields.grossAmount != null && fields.grossAmount > 0 ? [`${labels.gross}: ${formatEur(fields.grossAmount)} €`] : []),
60
+ ];
61
+ return `${footnoteMarker} ${parts.join('; ')};`;
62
+ }
@@ -1,58 +1,58 @@
1
- // Autobahn (auto.de redesign) — Preisbewertung rating math, extracted from CommonUtils.ts.
2
- //
3
- // Pulled out on purpose: CommonUtils.ts transitively imports `../constants/Search`, which
4
- // `require()`s image assets (webpack-only — breaks any plain Node `require`/`tsc` consumer).
5
- // This file only imports `../constants/price` (zero further imports) — a pure, testable leaf
6
- // module. CommonUtils.ts re-exports these for existing consumers — one implementation, not a fork.
7
- // (image-generator's Meta AIA renderer vendors a copy of this math directly instead of depending
8
- // on this package — see services/image-generator/src/services/metaAia/vendoredAutobahnMath.ts —
9
- // so a plain-Node backend doesn't need to npm-install this whole design system's dependency tree.)
10
- import { priceRatings, priceRatingConfig } from '../constants/price';
11
- import { PREDICTABLE_PRICE_INDEX } from '../constants/core';
12
-
13
- export const getPriceRatingConfig = (currentPrice: number, pricePredicted: number) => { // tslint:disable-line
14
- const round = (value: number) => {
15
- return Number.isFinite(pricePredicted)
16
- ? Math.round((pricePredicted * (1 + (value / 100))) / 100) * 100 // tslint:disable-line
17
- : '???';
18
- };
19
-
20
- if (currentPrice === 0) {
21
- console.log('Empty current price');
22
- }
23
-
24
- return Object.keys(priceRatingConfig).reduce((acc: any, key: any) => {
25
- const { min, max } = priceRatingConfig[key];
26
- return { ...acc, [key]: { min: round(min), max: round(max) } };
27
- }, {}); // tslint:disable-line
28
- };
29
-
30
- export const getPredictablePrice = (currentSalesPrice: number, predictablePrice: number) => {
31
- if (Number.isFinite(Number.parseFloat(`${predictablePrice}`))) {
32
- return predictablePrice;
33
- }
34
-
35
- return currentSalesPrice * PREDICTABLE_PRICE_INDEX;
36
- }
37
-
38
- export const getPriceRating = (currentSalesPrice: number, predictablePrice: number) => {
39
- const isCorrectCurrentSalesPrice = Number.isFinite(Number.parseFloat(`${currentSalesPrice}`));
40
- // const isCorrectPredictablePrice = Number.isFinite(Number.parseFloat(`${predictablePrice}`));
41
-
42
- if (!isCorrectCurrentSalesPrice) return 0;
43
-
44
-
45
- const validatedPredictablePrice = getPredictablePrice(currentSalesPrice, predictablePrice);
46
-
47
- const ratingConfig = getPriceRatingConfig(currentSalesPrice, validatedPredictablePrice);
48
-
49
- for (const key of Object.keys(ratingConfig)) {
50
- const { min, max } = ratingConfig[key];
51
- const TOP_PRICE = 5;
52
- if (key === 'highPrice' && currentSalesPrice === max) return 1;
53
- if (key === 'topPrice' && currentSalesPrice <= min) return TOP_PRICE;
54
- if (currentSalesPrice >= min && currentSalesPrice < max) return priceRatings.indexOf(key);
55
- }
56
-
57
- return 0;
58
- };
1
+ // Autobahn (auto.de redesign) — Preisbewertung rating math, extracted from CommonUtils.ts.
2
+ //
3
+ // Pulled out on purpose: CommonUtils.ts transitively imports `../constants/Search`, which
4
+ // `require()`s image assets (webpack-only — breaks any plain Node `require`/`tsc` consumer).
5
+ // This file only imports `../constants/price` (zero further imports) — a pure, testable leaf
6
+ // module. CommonUtils.ts re-exports these for existing consumers — one implementation, not a fork.
7
+ // (image-generator's Meta AIA renderer vendors a copy of this math directly instead of depending
8
+ // on this package — see services/image-generator/src/services/metaAia/vendoredAutobahnMath.ts —
9
+ // so a plain-Node backend doesn't need to npm-install this whole design system's dependency tree.)
10
+ import { priceRatings, priceRatingConfig } from '../constants/price';
11
+ import { PREDICTABLE_PRICE_INDEX } from '../constants/core';
12
+
13
+ export const getPriceRatingConfig = (currentPrice: number, pricePredicted: number) => { // tslint:disable-line
14
+ const round = (value: number) => {
15
+ return Number.isFinite(pricePredicted)
16
+ ? Math.round((pricePredicted * (1 + (value / 100))) / 100) * 100 // tslint:disable-line
17
+ : '???';
18
+ };
19
+
20
+ if (currentPrice === 0) {
21
+ console.log('Empty current price');
22
+ }
23
+
24
+ return Object.keys(priceRatingConfig).reduce((acc: any, key: any) => {
25
+ const { min, max } = priceRatingConfig[key];
26
+ return { ...acc, [key]: { min: round(min), max: round(max) } };
27
+ }, {}); // tslint:disable-line
28
+ };
29
+
30
+ export const getPredictablePrice = (currentSalesPrice: number, predictablePrice: number) => {
31
+ if (Number.isFinite(Number.parseFloat(`${predictablePrice}`))) {
32
+ return predictablePrice;
33
+ }
34
+
35
+ return currentSalesPrice * PREDICTABLE_PRICE_INDEX;
36
+ }
37
+
38
+ export const getPriceRating = (currentSalesPrice: number, predictablePrice: number) => {
39
+ const isCorrectCurrentSalesPrice = Number.isFinite(Number.parseFloat(`${currentSalesPrice}`));
40
+ // const isCorrectPredictablePrice = Number.isFinite(Number.parseFloat(`${predictablePrice}`));
41
+
42
+ if (!isCorrectCurrentSalesPrice) return 0;
43
+
44
+
45
+ const validatedPredictablePrice = getPredictablePrice(currentSalesPrice, predictablePrice);
46
+
47
+ const ratingConfig = getPriceRatingConfig(currentSalesPrice, validatedPredictablePrice);
48
+
49
+ for (const key of Object.keys(ratingConfig)) {
50
+ const { min, max } = ratingConfig[key];
51
+ const TOP_PRICE = 5;
52
+ if (key === 'highPrice' && currentSalesPrice === max) return 1;
53
+ if (key === 'topPrice' && currentSalesPrice <= min) return TOP_PRICE;
54
+ if (currentSalesPrice >= min && currentSalesPrice < max) return priceRatings.indexOf(key);
55
+ }
56
+
57
+ return 0;
58
+ };
@@ -196,27 +196,87 @@ export const ToggleRow: React.FunctionComponent<{
196
196
  className?: string;
197
197
  /** cash mode: the package is always included — shows a static "inklusive" note instead of the switch. */
198
198
  staticNote?: string;
199
- }> = ({ label, price, checked, disabled, onChange, className, staticNote }) => (
200
- <div className={className} style={{ display: 'flex', alignItems: 'center', justifyContent: staticNote ? undefined : 'space-between', gap: 10 }}>
201
- <span className="md-title-sm" style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
202
- {label}
203
- <Icon name="info" size={13} color="var(--md-on-surface-variant)" />
204
- </span>
205
- <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginLeft: staticNote ? 'auto' : undefined }}>
206
- {price && (
207
- <span className="num" style={{ fontWeight: 700, fontSize: 13.5, marginRight: staticNote ? 'auto' : undefined }}>
208
- {price}
209
- </span>
210
- )}
211
- {staticNote ? (
212
- <span style={{ fontFamily: 'Arial', fontWeight: 400, fontSize: 15, lineHeight: '17px', color: '#0E2238' }}>
213
- {staticNote}
214
- </span>
215
- ) : (
216
- <Switch checked={checked} disabled={disabled} onChange={(e) => onChange(e.target.checked)} ariaLabel={label} />
199
+ /** when set, the info icon becomes clickable and opens a popover with this body (e.g. the
200
+ * Rundum-Sorglos-Paket contents) — "Mit Schlussrate" rows omit this, keeping their icon inert. */
201
+ tooltip?: React.ReactNode;
202
+ /** aria-label for the popover's close button — required together with `tooltip`. */
203
+ tooltipCloseLabel?: string;
204
+ }> = ({ label, price, checked, disabled, onChange, className, staticNote, tooltip, tooltipCloseLabel }) => {
205
+ const [tooltipOpen, setTooltipOpen] = useState(false);
206
+ return (
207
+ <div
208
+ className={className}
209
+ style={{
210
+ display: 'flex',
211
+ alignItems: 'center',
212
+ justifyContent: staticNote ? undefined : 'space-between',
213
+ gap: 10,
214
+ position: tooltip ? 'relative' : undefined,
215
+ }}
216
+ >
217
+ <span
218
+ className="md-title-sm"
219
+ style={{ display: 'inline-flex', alignItems: 'center', gap: 5, cursor: tooltip ? 'pointer' : undefined }}
220
+ role={tooltip ? 'button' : undefined}
221
+ tabIndex={tooltip ? 0 : undefined}
222
+ onClick={tooltip ? (e) => { e.stopPropagation(); setTooltipOpen((v) => !v); } : undefined}
223
+ >
224
+ {label}
225
+ {tooltip && <Icon name="info" size={13} color="var(--md-on-surface-variant)" />}
226
+ </span>
227
+ {tooltip && tooltipOpen && (
228
+ <div className="pp-uvp-tooltip fin-toggle-tooltip" onClick={(e) => e.stopPropagation()}>
229
+ <button
230
+ type="button"
231
+ className="pp-uvp-tooltip-close fin-toggle-tooltip-close"
232
+ aria-label={tooltipCloseLabel}
233
+ onClick={(e) => {
234
+ e.stopPropagation();
235
+ setTooltipOpen(false);
236
+ }}
237
+ >
238
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
239
+ <path d="M5 5L15 15M15 5L5 15" stroke="white" strokeWidth={2.4} strokeLinecap="round" />
240
+ </svg>
241
+ </button>
242
+ <div className="pp-uvp-tooltip-body fin-toggle-tooltip-body">{tooltip}</div>
243
+ </div>
217
244
  )}
245
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginLeft: staticNote ? 'auto' : undefined }}>
246
+ {price && (
247
+ <span className="num" style={{ fontWeight: 700, fontSize: 13.5, marginRight: staticNote ? 'auto' : undefined }}>
248
+ {price}
249
+ </span>
250
+ )}
251
+ {staticNote ? (
252
+ <span style={{ fontFamily: 'Arial', fontWeight: 400, fontSize: 15, lineHeight: '17px', color: '#0E2238' }}>
253
+ {staticNote}
254
+ </span>
255
+ ) : (
256
+ <Switch checked={checked} disabled={disabled} onChange={(e) => onChange(e.target.checked)} ariaLabel={label} />
257
+ )}
258
+ </div>
218
259
  </div>
219
- </div>
260
+ );
261
+ };
262
+
263
+ /** body content for the "Rundum-Sorglos-Paket" ToggleRow tooltip — intro sentence, bold heading,
264
+ * then the package's contents as a bullet list. Shared by every addServicesToggle ToggleRow
265
+ * (FinanceCalculator's own card + VdpModules' modal/mobile-sheet instances). */
266
+ export const AddServicesTooltipBody: React.FunctionComponent<{ t: TranslationFunction }> = ({ t }) => (
267
+ <>
268
+ <p style={{ margin: '0 0 8px' }}>{t('autobahn:finance.addServicesTooltipIntro')}</p>
269
+ <p style={{ fontFamily: 'Arial', fontWeight: 700, fontSize: 10, lineHeight: '13px', color: '#0E2238', margin: '0 0 4px' }}>
270
+ {t('autobahn:finance.addServicesTooltipHeading')}
271
+ </p>
272
+ <ul style={{ margin: 0, padding: '0 0 0 14px' }}>
273
+ {t('autobahn:finance.addServicesTooltipItems')
274
+ .split('\n')
275
+ .map((item) => (
276
+ <li key={item}>{item}</li>
277
+ ))}
278
+ </ul>
279
+ </>
220
280
  );
221
281
 
222
282
  export interface FinanceCalculatorProps {
@@ -278,6 +338,12 @@ export interface FinanceCalculatorProps {
278
338
  phone?: string;
279
339
  /** primary CTA click (Angebot sichern). */
280
340
  onSecure?: () => void;
341
+ /** true once the customer already has an offer request/checkout in progress for this car —
342
+ * turns the primary CTA green (see .fin-cta-secured) instead of the default filled style. */
343
+ secured?: boolean;
344
+ /** overrides the primary CTA label while `secured` is true (e.g. "Zur Bestellung"). Ignored
345
+ * when `secured` is false — the CTA keeps its own default label in that case. */
346
+ secureLabel?: string;
281
347
  /** secondary CTA click (phone). */
282
348
  onCall?: () => void;
283
349
  /** open the Finanzierungs-/Leasingdetails popup (full breakdown + legal, expanded). Omit to hide the link. */
@@ -374,6 +440,8 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
374
440
  available,
375
441
  phone = '0800 40 30 182',
376
442
  onSecure,
443
+ secured,
444
+ secureLabel,
377
445
  onCall,
378
446
  onShowDetails,
379
447
  onOpenAdjust,
@@ -393,10 +461,10 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
393
461
  // price-evaluation tier from the rating label (defaults to 'Guter Preis' tier when unmapped).
394
462
  const tierIdx = TIER_LABELS.indexOf(ratingLabel as (typeof TIER_LABELS)[number]);
395
463
  const evalTier = tierIdx >= 0 ? tierIdx : 1;
396
- // both green tiers ('top' and 'good') collapse onto one "Top-Preis" claim (same convention as
397
- // CarCard's PriceEvalCol) — footnote 4 carries the disclaimer text.
398
464
  const isGoodTier = TIER_KEYS[evalTier] === 'good';
399
- const evalTierLabel = isGoodTier ? t('autobahn:tier.top') : t('autobahn:tier.' + TIER_KEYS[evalTier]);
465
+ // each tier shows its own real label now (Top/Guter/Fairer/Erhöht/Hoher Preis) — no more
466
+ // collapsing 'good' onto the "Top-Preis" claim. Pill styling (colour/class/footnote) unchanged.
467
+ const evalTierLabel = t('autobahn:tier.' + TIER_KEYS[evalTier]);
400
468
  const evalPillColor = isGoodTier ? TIER_COLORS[0] : TIER_COLORS[evalTier];
401
469
  // condition summary (current selection) + its own details link — financing: "0 € Anzahlung ·
402
470
  // 60 Monate · Finanzierungsdetails"; leasing: "25.000 € Sonderzahlung · 60 Mon. · 10.000
@@ -456,8 +524,8 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
456
524
  >
457
525
  {k === 'cash' ? (
458
526
  <>
459
- {/* mockup names the cash tab "Kaufen" on phone; desktop keeps "Barzahlung" */}
460
- <span className="hide-mobile">{t(labelKey)}</span>
527
+ {/* cash tab is "Kaufen" on both phone and desktop now — same key both spans. */}
528
+ <span className="hide-mobile">{t('autobahn:card.price.buy')}</span>
461
529
  <span className="show-mobile" style={{ justifyContent: 'center' }}>
462
530
  {t('autobahn:card.price.buy')}
463
531
  </span>
@@ -636,7 +704,10 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
636
704
  3
637
705
  </span>
638
706
  {uvpTooltipOpen && (
639
- <div className="pp-uvp-tooltip" onClick={(e) => e.stopPropagation()}>
707
+ <div
708
+ className={'pp-uvp-tooltip' + (mode === 'leasing' ? ' pp-uvp-tooltip--leasing' : '')}
709
+ onClick={(e) => e.stopPropagation()}
710
+ >
640
711
  <button
641
712
  type="button"
642
713
  className="pp-uvp-tooltip-close"
@@ -737,17 +808,29 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
737
808
  </button>
738
809
  {adjust &&
739
810
  (isCash ? (
740
- <div style={{ marginTop: 4 }}>
741
- <CalcRow label={t('autobahn:finance.paymentLabel')}>{t('autobahn:finance.paymentValue')}</CalcRow>
742
- <CalcRow label={t('autobahn:finance.warrantyLabel')} last={!available}>
743
- {t('autobahn:finance.warrantyValue')}
744
- </CalcRow>
745
- {available && (
746
- <CalcRow label={t('autobahn:finance.availabilityLabel')} last>
747
- {t('autobahn:finance.availabilityValue')}
748
- </CalcRow>
811
+ <>
812
+ <div style={{ marginTop: 4 }}>
813
+ <CalcRow label={t('autobahn:finance.paymentLabel')} last={!available && !(addServicesCost != null && onToggleAddServices)}>{t('autobahn:finance.paymentValue')}</CalcRow>
814
+ {available && (
815
+ <CalcRow label={t('autobahn:finance.availabilityLabel')} last={!(addServicesCost != null && onToggleAddServices)}>
816
+ {t('autobahn:finance.availabilityValue')}
817
+ </CalcRow>
818
+ )}
819
+ </div>
820
+ {addServicesCost != null && onToggleAddServices && (
821
+ <ToggleRow
822
+ className="fin-toggle-addservices fin-toggle-addservices--cash"
823
+ label={t('autobahn:finance.addServicesToggle')}
824
+ price={t('autobahn:finance.addServicesOnce', { value: eur(Math.round(addServicesCost)) })}
825
+ checked={!!withAddServices}
826
+ disabled={addServicesDisabled}
827
+ onChange={onToggleAddServices}
828
+ staticNote={t('autobahn:finance.addServicesIncluded')}
829
+ tooltip={<AddServicesTooltipBody t={t} />}
830
+ tooltipCloseLabel={t('autobahn:modal.close')}
831
+ />
749
832
  )}
750
- </div>
833
+ </>
751
834
  ) : (
752
835
  <div style={{ display: 'flex', flexDirection: 'column', marginTop: 8 }}>
753
836
  <div style={{ marginBottom: 15 }}>
@@ -808,6 +891,8 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
808
891
  checked={!!withAddServices}
809
892
  disabled={addServicesDisabled}
810
893
  onChange={onToggleAddServices}
894
+ tooltip={<AddServicesTooltipBody t={t} />}
895
+ tooltipCloseLabel={t('autobahn:modal.close')}
811
896
  />
812
897
  )}
813
898
  </div>
@@ -815,8 +900,16 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
815
900
  </div>
816
901
 
817
902
  {/* REQUEST CTA */}
818
- <Button variant="filled" block size="lg" icon="shield-check" style={{ marginTop: 14 }} onClick={onSecure}>
819
- {t('autobahn:finance.secureOffer')}
903
+ <Button
904
+ variant="filled"
905
+ block
906
+ size="lg"
907
+ icon="shield-check"
908
+ className={secured ? 'fin-cta-secured' : undefined}
909
+ style={{ marginTop: 14 }}
910
+ onClick={onSecure}
911
+ >
912
+ {secured && secureLabel ? secureLabel : t('autobahn:finance.secureOffer')}
820
913
  </Button>
821
914
  {/* phone CTA — touch only (hidden on desktop via .fin-call) */}
822
915
  <div className="fin-call">
@@ -848,11 +941,12 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
848
941
  <Button
849
942
  variant="filled"
850
943
  size="lg"
944
+ className={secured ? 'fin-cta-secured' : undefined}
851
945
  style={{ width: 216, height: 50, paddingLeft: 0, paddingRight: 9, gap: 18 }}
852
946
  onClick={onSecure}
853
947
  >
854
- <DocCheckIcon size={23} />
855
- {t('autobahn:finance.secureOffer')}
948
+ {secured ? <Icon name="shield-check" size={18} /> : <DocCheckIcon size={23} />}
949
+ {secured && secureLabel ? secureLabel : t('autobahn:finance.secureOffer')}
856
950
  </Button>
857
951
  <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
858
952
  <Button variant="outlined" size="lg" icon="phone" style={{ width: 112, height: 50 }} onClick={onCall}>