- Backend: validate display.currency against 10 supported ISO 4217 codes (USD, GBP, EUR, CAD, AUD, JPY, CHF, MXN, BRL, INR); return 400 on unsupported code with a clear message listing accepted values - Frontend: useCurrency composable fetches rates from open.er-api.com with 1-hour module-level cache and in-flight deduplication; falls back to USD display on network failure - Preferences store: adds display.currency with localStorage fallback for anonymous users and localStorage-to-DB migration for newly logged-in users - ListingCard: price and market price now convert from USD using live rates, showing USD synchronously while rates load then updating reactively - Settings UI: currency selector dropdown in Appearance section using theme-aware CSS classes; available to all users (anon via localStorage, logged-in via DB preference) - Tests: 6 Python tests for the PATCH /api/preferences currency endpoint (including ordering-safe fixture using patch.object on _LOCAL_SNIPE_DB); 14 Vitest tests for convertFromUSD, formatPrice, and formatPriceUSD
130 lines
3.8 KiB
TypeScript
130 lines
3.8 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import { useSessionStore } from './session'
|
|
|
|
export interface UserPreferences {
|
|
affiliate?: {
|
|
opt_out?: boolean
|
|
byok_ids?: {
|
|
ebay?: string
|
|
}
|
|
}
|
|
community?: {
|
|
blocklist_share?: boolean
|
|
}
|
|
display?: {
|
|
currency?: string
|
|
}
|
|
}
|
|
|
|
const CURRENCY_LS_KEY = 'snipe:currency'
|
|
const DEFAULT_CURRENCY = 'USD'
|
|
|
|
const apiBase = (import.meta.env.VITE_API_BASE as string) ?? ''
|
|
|
|
export const usePreferencesStore = defineStore('preferences', () => {
|
|
const session = useSessionStore()
|
|
const prefs = ref<UserPreferences>({})
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
const affiliateOptOut = computed(() => prefs.value.affiliate?.opt_out ?? false)
|
|
const affiliateByokId = computed(() => prefs.value.affiliate?.byok_ids?.ebay ?? '')
|
|
const communityBlocklistShare = computed(() => prefs.value.community?.blocklist_share ?? false)
|
|
|
|
// displayCurrency: DB preference for logged-in users, localStorage for anon users
|
|
const displayCurrency = computed((): string => {
|
|
return prefs.value.display?.currency ?? DEFAULT_CURRENCY
|
|
})
|
|
|
|
async function load() {
|
|
if (!session.isLoggedIn) {
|
|
// Anonymous user: read currency from localStorage
|
|
const stored = localStorage.getItem(CURRENCY_LS_KEY)
|
|
if (stored) {
|
|
prefs.value = { ...prefs.value, display: { ...prefs.value.display, currency: stored } }
|
|
}
|
|
return
|
|
}
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const res = await fetch(`${apiBase}/api/preferences`)
|
|
if (res.ok) {
|
|
const data: UserPreferences = await res.json()
|
|
// Migration: if logged in but no DB preference, fall back to localStorage value
|
|
if (!data.display?.currency) {
|
|
const lsVal = localStorage.getItem(CURRENCY_LS_KEY)
|
|
if (lsVal) {
|
|
data.display = { ...data.display, currency: lsVal }
|
|
}
|
|
}
|
|
prefs.value = data
|
|
}
|
|
} catch {
|
|
// Non-cloud deploy or network error — preferences unavailable
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function setPref(path: string, value: boolean | string | null) {
|
|
if (!session.isLoggedIn) return
|
|
error.value = null
|
|
try {
|
|
const res = await fetch(`${apiBase}/api/preferences`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ path, value }),
|
|
})
|
|
if (res.ok) {
|
|
prefs.value = await res.json()
|
|
} else {
|
|
const data = await res.json().catch(() => ({}))
|
|
error.value = data.detail ?? 'Failed to save preference.'
|
|
}
|
|
} catch {
|
|
error.value = 'Network error saving preference.'
|
|
}
|
|
}
|
|
|
|
async function setAffiliateOptOut(value: boolean) {
|
|
await setPref('affiliate.opt_out', value)
|
|
}
|
|
|
|
async function setAffiliateByokId(id: string) {
|
|
// Empty string clears the BYOK ID (router falls back to CF env var)
|
|
await setPref('affiliate.byok_ids.ebay', id.trim() || null)
|
|
}
|
|
|
|
async function setCommunityBlocklistShare(value: boolean) {
|
|
await setPref('community.blocklist_share', value)
|
|
}
|
|
|
|
async function setDisplayCurrency(code: string) {
|
|
const upper = code.toUpperCase()
|
|
// Optimistic local update so the UI reacts immediately
|
|
prefs.value = { ...prefs.value, display: { ...prefs.value.display, currency: upper } }
|
|
if (session.isLoggedIn) {
|
|
await setPref('display.currency', upper)
|
|
} else {
|
|
// Anonymous user: persist to localStorage only
|
|
localStorage.setItem(CURRENCY_LS_KEY, upper)
|
|
}
|
|
}
|
|
|
|
return {
|
|
prefs,
|
|
loading,
|
|
error,
|
|
affiliateOptOut,
|
|
affiliateByokId,
|
|
communityBlocklistShare,
|
|
displayCurrency,
|
|
load,
|
|
setAffiliateOptOut,
|
|
setAffiliateByokId,
|
|
setCommunityBlocklistShare,
|
|
setDisplayCurrency,
|
|
}
|
|
})
|