mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
feat(settings): make the display currency optional, falling back to the trip's
Costs already resolved `default_currency || trip.currency || 'EUR'`, but the setting could never actually be empty: the store seeded 'USD', so a user who had never touched it silently had every trip converted into dollars, and the picker offered no way to unset it. Seed it empty and lead the picker with a "Trip currency" option, so an unset preference means "show each trip in its own currency" instead of forcing them all through one code. An explicit empty value persists and beats the admin-set instance default — it is a deliberate choice, not an absence. This also brings the public share's fallback to life: the share payload has resolved sharer's currency -> trip currency since #1361, but the trip-currency branch was unreachable while every owner had a currency forced on them. Plugins are handed `formats.currency` as a concrete code, so PluginFrame now resolves the same chain rather than passing an empty string through the bridge.
This commit is contained in:
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { useTripStore } from '../../store/tripStore'
|
||||
import { useToast } from '../shared/Toast'
|
||||
import ConfirmDialog from '../shared/ConfirmDialog'
|
||||
import { pluginsApi } from '../../api/client'
|
||||
@@ -141,6 +142,10 @@ export default function PluginFrame({ pluginId, tripId = null, placeId = null, d
|
||||
const userAvatar = useAuthStore((s) => s.user?.avatar_url ?? null)
|
||||
const isAdmin = useAuthStore((s) => s.user?.role === 'admin')
|
||||
const settings = useSettingsStore((s) => s.settings)
|
||||
// Plugins format money against a concrete code, so resolve the same chain Costs
|
||||
// uses: the user's display currency, else the trip's own.
|
||||
const tripCurrency = useTripStore((s) => s.trip?.currency)
|
||||
const displayCurrency = (settings.default_currency || tripCurrency || 'EUR').toUpperCase()
|
||||
const [height, setHeight] = useState<number | null>(null)
|
||||
// A host-rendered ConfirmDialog on the plugin's behalf: sandboxed frames have no
|
||||
// allow-modals and can't overlay the host, so destructive plugin actions get the
|
||||
@@ -177,7 +182,7 @@ export default function PluginFrame({ pluginId, tripId = null, placeId = null, d
|
||||
appearance: readAppearance(),
|
||||
formats: {
|
||||
locale,
|
||||
currency: settings.default_currency,
|
||||
currency: displayCurrency,
|
||||
timeFormat: settings.time_format,
|
||||
distanceUnit: settings.distance_unit,
|
||||
temperatureUnit: settings.temperature_unit,
|
||||
|
||||
@@ -40,13 +40,18 @@ export default function DisplaySettingsTab(): React.ReactElement {
|
||||
{/* Display currency */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-content-secondary">{t('settings.currency')}</label>
|
||||
{/* Unset ('') means "no personal preference": Costs then shows each trip in its
|
||||
own currency, instead of forcing every trip through one display currency. */}
|
||||
<CustomSelect
|
||||
value={settings.default_currency || 'EUR'}
|
||||
value={settings.default_currency || ''}
|
||||
onChange={async v => {
|
||||
try { await updateSetting('default_currency', String(v)) }
|
||||
catch (e: unknown) { toast.error(e instanceof Error ? e.message : t('common.error')) }
|
||||
}}
|
||||
options={currenciesWith(settings.default_currency).map(c => ({ value: c, label: `${c} — ${SYMBOLS[c] || c}` }))}
|
||||
options={[
|
||||
{ value: '', label: t('settings.currencyTrip') },
|
||||
...currenciesWith(settings.default_currency || '').map(c => ({ value: c, label: `${c} — ${SYMBOLS[c] || c}` })),
|
||||
]}
|
||||
searchable
|
||||
/>
|
||||
<p className="text-xs text-content-faint mt-2">{t('settings.currencyHint')}</p>
|
||||
|
||||
@@ -28,7 +28,8 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
||||
default_lng: 2.3522,
|
||||
default_zoom: 10,
|
||||
dark_mode: false,
|
||||
default_currency: 'USD',
|
||||
// Empty = no personal display currency, so Costs falls back to the trip's own.
|
||||
default_currency: '',
|
||||
language: localStorage.getItem('app_language') || 'en',
|
||||
temperature_unit: 'fahrenheit',
|
||||
distance_unit: 'metric',
|
||||
|
||||
+2
-1
@@ -110,7 +110,8 @@ export interface Settings {
|
||||
default_lng: number
|
||||
default_zoom: number
|
||||
dark_mode: boolean | string
|
||||
default_currency: string
|
||||
/** Display currency for Costs. Empty/null = follow each trip's own currency. */
|
||||
default_currency: string | null
|
||||
language: string
|
||||
temperature_unit: string
|
||||
distance_unit?: DistanceUnit
|
||||
|
||||
@@ -158,7 +158,10 @@ describe('settingsStore', () => {
|
||||
|
||||
const state = useSettingsStore.getState();
|
||||
expect(state.settings.dark_mode).toBe(true);
|
||||
expect(state.settings.default_currency).toBe('USD');
|
||||
expect(state.settings.language).toBe('en');
|
||||
// No display currency of their own: Costs then follows each trip's own currency
|
||||
// rather than forcing every trip through one code.
|
||||
expect(state.settings.default_currency).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -290,6 +290,7 @@ const settings: TranslationStrings = {
|
||||
'settings.about.supporter.tier.hostelBunkmate': 'Hostel Bunkmate', // en-fallback
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': 'عملة الرحلة',
|
||||
'settings.passkey.title': 'مفاتيح المرور',
|
||||
'settings.passkey.description':
|
||||
'سجّل الدخول بشكل أسرع وأكثر مقاومة للتصيّد باستخدام مفتاح مرور — ببصمة إصبعك أو وجهك أو رمز PIN أو مفتاح أمان مادي. تبقى كلمة المرور كنسخة احتياطية.',
|
||||
|
||||
@@ -303,6 +303,7 @@ const settings: TranslationStrings = {
|
||||
'settings.notificationPreferences.ntfy': 'Ntfy',
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': 'Moeda da viagem',
|
||||
'settings.passkey.title': 'Passkeys',
|
||||
'settings.passkey.description':
|
||||
'Entre mais rápido e com proteção contra phishing usando uma passkey — sua impressão digital, rosto, PIN ou uma chave de segurança física. Sua senha continua disponível como reserva.',
|
||||
|
||||
@@ -297,6 +297,7 @@ const settings: TranslationStrings = {
|
||||
'settings.notificationPreferences.ntfy': 'Ntfy',
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': 'Měna cesty',
|
||||
'settings.passkey.title': 'Přístupové klíče',
|
||||
'settings.passkey.description':
|
||||
'Přihlašujte se rychleji a s ochranou proti phishingu pomocí přístupového klíče — otiskem prstu, obličejem, PINem nebo hardwarovým klíčem. Vaše heslo zůstává jako záloha.',
|
||||
|
||||
@@ -305,6 +305,7 @@ const settings: TranslationStrings = {
|
||||
'settings.notificationPreferences.ntfy': 'Ntfy',
|
||||
'settings.currency': 'Währung',
|
||||
'settings.currencyHint': 'Alle Beträge in Costs werden in diese Währung umgerechnet und angezeigt.',
|
||||
'settings.currencyTrip': 'Reisewährung',
|
||||
'settings.passkey.title': 'Passkeys',
|
||||
'settings.passkey.description':
|
||||
'Melde dich schneller und phishing-resistent mit einem Passkey an — per Fingerabdruck, Gesicht, PIN oder Hardware-Schlüssel. Dein Passwort bleibt als Backup erhalten.',
|
||||
|
||||
@@ -309,6 +309,7 @@ const settings: TranslationStrings = {
|
||||
'settings.mfa.demoBlocked': 'Not available in demo mode',
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': 'Trip currency',
|
||||
'settings.passkey.title': 'Passkeys',
|
||||
'settings.passkey.description':
|
||||
'Sign in faster and phishing-resistant with a passkey — your fingerprint, face, PIN, or a hardware key. Your password stays as a backup.',
|
||||
|
||||
@@ -305,6 +305,7 @@ const settings: TranslationStrings = {
|
||||
'settings.notificationPreferences.ntfy': 'Ntfy',
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': 'Moneda del viaje',
|
||||
'settings.passkey.title': 'Passkeys',
|
||||
'settings.passkey.description':
|
||||
'Inicia sesión más rápido y con protección frente al phishing usando una passkey: tu huella, tu cara, tu PIN o una llave de seguridad física. Tu contraseña sigue disponible como respaldo.',
|
||||
|
||||
@@ -308,6 +308,7 @@ const settings: TranslationStrings = {
|
||||
'settings.notificationPreferences.ntfy': 'Ntfy',
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': 'Devise du voyage',
|
||||
'settings.passkey.title': 'Passkeys',
|
||||
'settings.passkey.description':
|
||||
'Connectez-vous plus rapidement et de façon résistante au phishing avec une passkey — votre empreinte digitale, votre visage, votre code PIN ou une clé matérielle. Votre mot de passe reste disponible en secours.',
|
||||
|
||||
@@ -312,6 +312,7 @@ const settings: TranslationStrings = {
|
||||
'settings.mfa.demoBlocked': 'Δεν είναι διαθέσιμο σε λειτουργία demo',
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': 'Νόμισμα ταξιδιού',
|
||||
'settings.passkey.title': 'Passkeys',
|
||||
'settings.passkey.description':
|
||||
'Συνδεθείτε πιο γρήγορα και με προστασία από phishing χρησιμοποιώντας ένα passkey — το δαχτυλικό σας αποτύπωμα, το πρόσωπό σας, ένα PIN ή ένα κλειδί υλικού. Ο κωδικός σας παραμένει ως εφεδρεία.',
|
||||
|
||||
@@ -304,6 +304,7 @@ const settings: TranslationStrings = {
|
||||
'settings.notificationPreferences.ntfy': 'Ntfy',
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': 'Az utazás pénzneme',
|
||||
'settings.passkey.title': 'Passkey-k',
|
||||
'settings.passkey.description':
|
||||
'Jelentkezz be gyorsabban és adathalászat-állóan egy passkey-jel — ujjlenyomattal, arccal, PIN-kóddal vagy hardveres kulccsal. A jelszavad tartalékként megmarad.',
|
||||
|
||||
@@ -301,6 +301,7 @@ const settings: TranslationStrings = {
|
||||
'settings.bookingLabelsHint': 'Menampilkan nama stasiun / bandara di peta. Jika mati, hanya ikon ditampilkan.',
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': 'Mata uang perjalanan',
|
||||
'settings.passkey.title': 'Passkey',
|
||||
'settings.passkey.description':
|
||||
'Masuk lebih cepat dan tahan terhadap phishing dengan passkey — sidik jari, wajah, PIN, atau kunci keamanan fisik kamu. Kata sandimu tetap tersedia sebagai cadangan.',
|
||||
|
||||
@@ -303,6 +303,7 @@ const settings: TranslationStrings = {
|
||||
'settings.notificationPreferences.ntfy': 'Ntfy',
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': 'Valuta del viaggio',
|
||||
'settings.passkey.title': 'Passkey',
|
||||
'settings.passkey.description':
|
||||
'Accedi più velocemente e in modo resistente al phishing con una passkey — la tua impronta digitale, il volto, il PIN o una chiave hardware. La tua password resta come riserva.',
|
||||
|
||||
@@ -278,6 +278,7 @@ const settings: TranslationStrings = {
|
||||
'settings.oauth.badge.machine': 'マシン',
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': '旅行の通貨',
|
||||
'settings.passkey.title': 'パスキー',
|
||||
'settings.passkey.description':
|
||||
'指紋、顔認証、PIN、またはハードウェアキーを使うパスキーで、より速く、フィッシングに強いサインインができます。パスワードはバックアップとして残ります。',
|
||||
|
||||
@@ -294,6 +294,7 @@ const settings: TranslationStrings = {
|
||||
'settings.oauth.badge.machine': '머신',
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': '여행 통화',
|
||||
'settings.passkey.title': '패스키',
|
||||
'settings.passkey.description':
|
||||
'지문, 얼굴, PIN 또는 하드웨어 키 같은 패스키로 더 빠르고 피싱에 강하게 로그인하세요. 비밀번호는 백업으로 그대로 유지됩니다.',
|
||||
|
||||
@@ -304,6 +304,7 @@ const settings: TranslationStrings = {
|
||||
'settings.notificationPreferences.ntfy': 'Ntfy',
|
||||
'settings.currency': 'Valuta',
|
||||
'settings.currencyHint': 'Alle bedragen onder Onkosten worden omgerekend naar en weergegeven in deze valuta.',
|
||||
'settings.currencyTrip': 'Valuta van de reis',
|
||||
'settings.passkey.title': 'Passkeys',
|
||||
'settings.passkey.description':
|
||||
'Log sneller en phishingbestendig in met een passkey — je vingerafdruk, gezicht, pincode of een hardwaresleutel. Je wachtwoord blijft als back-up bestaan.',
|
||||
|
||||
@@ -301,6 +301,7 @@ const settings: TranslationStrings = {
|
||||
'settings.mustChangePassword': 'Musisz zmienić hasło przed kontynuowaniem.',
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': 'Waluta wyjazdu',
|
||||
'settings.passkey.title': 'Klucze dostępu',
|
||||
'settings.passkey.description':
|
||||
'Loguj się szybciej i z odpornością na phishing za pomocą klucza dostępu — odcisku palca, twarzy, kodu PIN lub klucza sprzętowego. Twoje hasło pozostaje jako zapasowa opcja.',
|
||||
|
||||
@@ -302,6 +302,7 @@ const settings: TranslationStrings = {
|
||||
'settings.notificationPreferences.ntfy': 'Ntfy',
|
||||
'settings.currency': 'Валюта',
|
||||
'settings.currencyHint': 'Все расходы будут конвертированы и показаны в этой валюте.',
|
||||
'settings.currencyTrip': 'Валюта поездки',
|
||||
'settings.passkey.title': 'Passkeys',
|
||||
'settings.passkey.description':
|
||||
'Входите быстрее и с защитой от фишинга с помощью passkey — отпечатка пальца, лица, PIN-кода или аппаратного ключа. Ваш пароль остаётся как резервный способ.',
|
||||
|
||||
@@ -306,6 +306,7 @@ const settings: TranslationStrings = {
|
||||
'settings.mfa.demoBlocked': 'Finns inte i demoläge',
|
||||
'settings.currency': 'Valuta',
|
||||
'settings.currencyHint': 'Alla belopp under kostnader omräknas till och redovisas i denna valuta.',
|
||||
'settings.currencyTrip': 'Resans valuta',
|
||||
'settings.passkey.title': 'Inloggningsnycklar',
|
||||
'settings.passkey.description':
|
||||
'Logga in snabbare och med bättre skydd mot nätfiske med en inloggningsnyckel – ditt fingeravtryck, ditt ansikte, din PIN-kod eller en hårdvarunyckel. Ditt lösenord finns kvar som reserv.',
|
||||
|
||||
@@ -300,6 +300,7 @@ const settings: TranslationStrings = {
|
||||
'settings.oauth.badge.machine': 'makine',
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': 'Gezi para birimi',
|
||||
'settings.passkey.title': 'Passkey’ler',
|
||||
'settings.passkey.description':
|
||||
'Passkey ile daha hızlı ve kimlik avına dayanıklı şekilde oturum açın — parmak iziniz, yüzünüz, PIN’iniz veya bir donanım anahtarı. Şifreniz yedek olarak kalır.',
|
||||
|
||||
@@ -302,6 +302,7 @@ const settings: TranslationStrings = {
|
||||
'settings.oauth.badge.machine': 'машина',
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': 'Валюта подорожі',
|
||||
'settings.passkey.title': 'Passkeys',
|
||||
'settings.passkey.description':
|
||||
'Входьте швидше та з захистом від фішингу за допомогою passkey — відбитка пальця, обличчя, PIN-коду або апаратного ключа. Ваш пароль залишається як резервний варіант.',
|
||||
|
||||
@@ -308,6 +308,7 @@ const settings: TranslationStrings = {
|
||||
'settings.mfa.demoBlocked': 'Không có sẵn ở chế độ demo',
|
||||
'settings.currency': 'Tiền tệ',
|
||||
'settings.currencyHint': 'Tất cả số tiền trong Chi phí được chuyển đổi sang và hiển thị bằng loại tiền tệ này.',
|
||||
'settings.currencyTrip': 'Tiền tệ của chuyến đi',
|
||||
'settings.passkey.title': 'Mật mã',
|
||||
'settings.passkey.description':
|
||||
'Đăng nhập nhanh hơn và chống lừa đảo bằng mật mã — vân tay, khuôn mặt, PIN hoặc khóa phần cứng của bạn. Mật khẩu của bạn vẫn là bản sao lưu.',
|
||||
|
||||
@@ -285,6 +285,7 @@ const settings: TranslationStrings = {
|
||||
'settings.bookingLabelsHint': '在地圖上顯示車站 / 機場名稱。關閉時僅顯示圖示。',
|
||||
'settings.currency': 'Currency',
|
||||
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
|
||||
'settings.currencyTrip': '行程貨幣',
|
||||
'settings.passkey.title': 'Passkey',
|
||||
'settings.passkey.description':
|
||||
'使用 Passkey 更快登入,並可抵禦網路釣魚——透過你的指紋、臉部、PIN 碼或硬體金鑰。你的密碼仍會保留作為備援。',
|
||||
|
||||
@@ -283,6 +283,7 @@ const settings: TranslationStrings = {
|
||||
'settings.notificationPreferences.ntfy': 'Ntfy',
|
||||
'settings.currency': '货币',
|
||||
'settings.currencyHint': '费用中的所有金额都会换算并以此货币显示。',
|
||||
'settings.currencyTrip': '行程货币',
|
||||
'settings.passkey.title': '通行密钥',
|
||||
'settings.passkey.description':
|
||||
'使用通行密钥更快登录,并能抵御钓鱼攻击——通过指纹、面容、PIN 或硬件密钥验证。你的密码仍可作为备用方式。',
|
||||
|
||||
Reference in New Issue
Block a user