Changelog
Every release, every fix, every rate recalculation.
What changed, when, and whether it actually affects you.
March 2026
v1.4.0
Released 10 March 2026 — GitHub Release
The "post-payment automations silently do nothing" release. If you had multicurrency active and wondered why FluentCRM contacts weren't being tagged, FluentCommunity spaces weren't being populated, and membership grants weren't being created — this is why, and this is the fix.
Critical
- Post-payment integrations killed by multicurrency plugin — FluentCart fires
order_paid_donewith an event array (['order' => $order, 'customer' => $customer, ...]), not a raw Order object. Two multicurrency handlers at priority 10 —OrderSnapshotHooks::saveSnapshot()andFluentCrmSync::onOrderPaid()— were treating the array as an Order and calling->getMeta()on it. That throws aTypeError, which is\Throwablebut not\Exception. FluentCart's integration runner catches\Exceptiononly, so theTypeErrorkilled the entire hook chain at priority 10 — before FluentCart's integration feeds at priority 11 could execute. Every FluentCRM tag, every FluentCommunity enrollment, every membership grant, every Fakturownia invoice — silently dead. Payment succeeded, everything else didn't. Both handlers now extract the Order from either payload shape via a sharedFluentCartEvent::extractOrder()helper. - FluentCart settings page killed by
wp_send_json()inside a filter —MultiCurrencySettings::getGlobalFields()was registered as a WordPress filter callback but calledwp_send_json(), which callsdie(). Filters are supposed to return a value and let the caller decide what to do with it. Instead, the entire PHP process terminated mid-filter. FluentCart's own response wrapper and field defaults never ran. The settings page worked by accident — the response format was close enough that the Vue component didn't complain. Now returns the field array properly and lets FluentCart handle the response. - Save settings response wrapped incorrectly —
saveGlobalSettings()wrapped the response in{data: {message, status}}but FluentCart expects{message, status}directly. The extradatawrapper meant the success toast might not show in all FluentCart versions. Removed the wrapper.
Security
- Rate limiter and event log used proxy IP behind Cloudflare —
REMOTE_ADDRbehind a Cloudflare tunnel is the edge server IP, not the client's. Every visitor shared the same rate limit bucket (30 requests/minute total), and every event log entry had the same IP hash. One legitimate user could burn the limit for everyone. NewIpResolverchecksCF-Connecting-IPfirst (Cloudflare), thenX-Forwarded-For(generic proxy), thenREMOTE_ADDR(direct). Multi-IPX-Forwarded-Forheaders are parsed correctly — first IP wins.
Fixed
- Exchange rate insert failures silently ignored —
ExchangeRateRepository::insert()didn't check the return value of$wpdb->insert(). If the insert failed (table missing, disk full, connection error), the caller cached a rate that was never persisted. On the next cache miss, the rate vanished. Now returnsbooland logs the DB error on failure. - Resolver chain cached forever within a request —
ContextModule::$cachedChainwas set once and never reset. If admin settings changed mid-request (e.g. saving settings and then resolving context), the stale chain with old configuration was used.CurrencyContextService::reset()now also clears the cached resolver chain. - Event log timestamps in local time, rate history in UTC —
EventLogRepositoryusedcurrent_time('mysql')(local timezone), while rate history usedgmdate()(UTC). Cross-table queries and admin dashboards showed mismatched timestamps. GDPR exports also returned local-time timestamps. Now usesgmdate()consistently. - ECB provider used raw
error_log()instead of Logger — one strayerror_log()call bypassed the plugin's log level controls and would write to the PHP error log even with debug mode off. Now usesLogger::error()like everything else. - Guest currency preference saved without validation on login —
mergeGuestPreference()saved the cookie value to user meta without checking if the currency was still enabled. A stale cookie with a disabled currency code would persist indefinitely, retried on every page load. Now validates against the enabled currency list before saving. - Admin panel swallowed REST errors in silence — exchange rate loading, diagnostics refresh, and several other admin AJAX calls had no
.catch()handlers. A failed request would leave the loading spinner spinning indefinitely with no indication that anything went wrong. All REST API calls now surface proper error toasts via FluentCart's notification system, because a spinner that never stops is not an error message. - FluentCRM contact-not-found logged at wrong level — when a customer had no matching FluentCRM contact, the sync logged it at the same level as actual failures. Noisy logs full of expected non-events made real problems harder to spot. Now logs at debug level, where it belongs.
- FluentCRM custom field existence unchecked — the diagnostics tab checked for FluentCRM's presence but never verified that the required custom fields (
preferred_currency,last_order_currency) actually existed. Missing fields would cause silent sync failures with no admin-visible explanation. Diagnostics now includes a custom field health check that tells you exactly which fields are missing and need creating.
Features
- FluentCRM Smart Codes — new
mc_order.*smart code group for FluentCRM automation emails. Six codes:display_currency,base_currency,display_total,display_subtotal,exchange_rate, andcharged_notice. Your post-purchase automations can now show amounts in the customer's display currency instead of blindly defaulting to the store's base currency. Because sending "You paid $93.42" to someone who checked out in euros is how you lose trust in one email. - Public API — two new helper functions for theme and plugin developers:
fchub_mc_get_order_display_currency()returns the display currency from a past order's snapshot, andfchub_mc_format_order_price()formats a price using that order's stored currency and exchange rate. For anyone building custom order templates, receipts, or invoices who was tired of reverse-engineering the snapshot meta keys.
v1.3.0
Released 7 March 2026 — GitHub Release
The Gutenberg glow-up. The shortcode still works, but now it has a proper block-shaped colleague with six outfits and a whole family of friends.
Features
- Currency Switcher Gutenberg block — a proper block with six style presets (Default, Pill, Minimal, Subtle, Glass, Contrast), 20+ inspector controls, live editor preview, and shortcode transform. Drop it in headers, footers, template parts, synced patterns — anywhere blocks go. The shortcode era isn't over, but it has company now.
- Current Currency block — inline display of the active currency. Flag + code, code only, or full name. For sticky status bars and utility rows.
- Exchange Rate block — "1 PLN = 0.2350 EUR" with configurable precision. For the transparency enthusiasts.
- Currency Context Notice block — "Viewing prices in USD. Checkout charged in PLN." Trust-building disclosure without a dropdown.
- Currency Selector Buttons block — one button per currency, no dropdown. For stores with 3-4 currencies that don't need the ceremony.
- Currency Showcase pattern — all five blocks on the page in one click. Because manually assembling test pages is nobody's idea of fun.
- Admin live preview — the Switcher settings tab now has a sticky preview widget at the top. Same CSS classes, same SVG flags, instant updates. Change a preset, toggle a flag, see it move. No more save-refresh-squint-repeat.
- No-JS fallback that actually works — the old one was a trigger button with delusions of interactivity. Replaced with a real
<form>that POSTs with a nonce, validates the currency server-side, persists, and redirects back. Affects 0.3% of visitors and 100% of your accessibility audit.
Fixed
- Pill preset dropdown was oval —
--fchub-mc-radius: 999pxflowed uncapped into the dropdown container and option rows, producing beautifully rounded ovals instead of a rectangular list. Trigger stays pill-shaped, dropdown and options now capped at sensible radii. - Glass preset looked identical to Default in admin and editor —
backdrop-filter: blur(14px)was technically applied, but the preview backgrounds were solid white. Nothing behind it to blur = no glass effect. Both previews now swap to a colourful gradient background when glass is active, giving the blur something to work with. - Viewport-aware dropdown positioning — the dropdown now measures available space in every direction and picks the least-bad position instead of blindly trusting the preferred alignment. No more dropdowns vanishing off-screen or clipping behind the block toolbar.
v1.2.4
Released 7 March 2026 — GitHub Release
Turns out many managed WordPress hosts (Rocket.net, Cloudways, GridPane, etc.) block HTTP requests to any path containing /vendor/ — a blanket security rule to prevent direct access to Composer dependencies. Reasonable policy, collateral damage on our third-party library. Moved SortableJS out of admin/vendor/ to admin/lib/ so it stops getting caught in the crossfire.
Fixed
- SortableJS blocked by server-level
/vendor/path rules — managed hosting platforms commonly deny requests to any URL containing/vendor/to prevent exposure of Composer packages. Our vendoredSortable.min.jslived atadmin/vendor/Sortable.min.js— a perfectly innocent path that looked guilty by association. The server returned a 403 HTML page instead of JavaScript, which the browser parsed asUnexpected token '<'. Relocated toadmin/lib/Sortable.min.jswhere no security rule will touch it.
v1.2.3
Released 7 March 2026 — GitHub Release
Improved SortableJS asset delivery for environments where v1.2.2's handle fix wasn't sufficient.
Fixed
- SortableJS not loading on some hosting configurations — certain server environments with aggressive static asset caching or non-standard
plugin_dir_url()resolution could prevent the vendoredSortable.min.jsfrom being served, even after the handle collision fix in v1.2.2. Hardened the asset pipeline to ensure the library reaches the browser regardless of hosting quirks. If drag-and-drop wasn't working for you after updating to v1.2.2, this one's for you.
v1.2.2
Released 7 March 2026 — GitHub Release
Turns out the drag-and-drop currency reordering was silently broken for some users and we couldn't reproduce it. The SortableJS script handle was registered as sortablejs — a name generic enough that any other plugin registering the same handle first would silently hijack ours. WordPress doesn't throw, doesn't warn, just uses whoever registered first and moves on. The grab cursor worked (pure CSS), but the actual drag library never loaded. Classic WordPress script handle collision — the kind of bug that works perfectly on every dev machine and breaks in production.
Fixed
- Drag-and-drop currency reordering broken on some installs — the
sortablejsscript handle could collide with other plugins registering the same handle. WordPress silently drops duplicatewp_register_scriptcalls, so ourSortable.min.jsnever loaded while the CSScursor: grabkept pretending everything was fine. Renamed tofchub-mc-sortablejsso it can't be stolen. Also added a console warning when SortableJS fails to load, because silent failure is not a debugging strategy.
v1.2.1
Released 7 March 2026 — GitHub Release
One bug, one symptom: every price in the checkout order summary rendered 50% larger and lighter after switching currency. Turns out textContent is a wrecking ball.
Fixed
- Checkout prices rendered at wrong font size after currency switch —
findPriceTarget()didn't recognise FluentCart's nested price structure (.fct_line_item_totalinside.fct_line_item_price). SettingtextContenton the parent destroyed the styled inner<span>and itstext-sm font-mediumclasses. Prices jumped from 14px/500 to the body's 21px/300. Now drills into.fct_line_item_total,.fct_summary_value, and.fct_coupon_pricechild elements before falling back to the parent. Also marks the inner target as projected to prevent the selector loop from re-processing it. Your checkout no longer looks like it's shouting the prices at you.
v1.2.0
Released 7 March 2026 — GitHub Release
A deep audit found things hiding in the codebase that no single pair of eyes would have caught. This is the "we actually read every line" release. A second pass found the P1 we missed — order snapshots were reading the ambient request context instead of the customer's. Which means admin-completed orders, webhook payments, and guest checkouts could all get the wrong currency. Lovely.
Critical
- Order snapshots now captured at checkout, not payment —
saveSnapshot()ran onorder_paid_done, which fires via Action Scheduler — no cookies, no logged-in user, no shopper context. The earlier fallback to user meta was insufficient because the ambient context could be the admin's preference, guest orders had no user_id at all, and disabled currencies weren't validated. Now captures the snapshot duringcheckout/prepare_other_datawhile the customer's HTTP request is still warm. Theorder_paid_donehook stays as a fallback for manual/API orders, but validates against enabled currencies first. No context? No snapshot. Better than wrong.
Security
- Rate limiter IP spoofing fixed — the rate limiter was trusting the
X-Forwarded-Forheader, which is user-controllable. Your visitors were writing their own hall passes. Now usesREMOTE_ADDRexclusively. - Enabled switch actually enforced — setting Multi-Currency to "disabled" didn't actually disable the public
/contextendpoint, checkout disclosure injection, or order snapshot recording. The off switch was decorative. Now returns 403, skips disclosure, and skips snapshots when disabled. - Rate limiter no longer punishes invalid requests — the counter incremented before JSON parsing and currency validation. An attacker could burn your rate limit bucket with garbage requests, locking out real users. Counter now increments only after validation passes.
Fixed
- Order snapshots lost on webhook/IPN payments —
SaveOrderSnapshotActionresolved currency from the ambient request (cookies, logged-in user). In webhook, IPN, or admin-paid flows there's no shopper context, so snapshots silently disappeared — and CRM sync with them. Now falls back to the order customer's stored currency preference when ambient context is empty. - Modal checkout prices not converted —
.fct-modal-cs-line-pricewasn't in the selector list. Modal users saw base currency prices while the rest of the page was converted. - Coupon discount amounts stuck in base currency —
.fct_coupon_priceand.fct-coupon-priceselectors were missing. Your "– $25.00" coupon discount stayed in dollars while everything else was in euros. - Thank you page prices not converted — total, line items, and payment info selectors were all missing. Customers completed an order in EUR and got a receipt in USD. Confidence-inspiring.
- Pricing table payment type text stuck in base currency — "300.00zł per year for 12 cycles + 100.00zł setup fee" lived in a sibling element, not a child. The engine never knew it existed.
stripRegexalternating results —RegExp.test()with thegflag advanceslastIndexbetween calls, so the bare-number guard returned different results on consecutive calls with the same input. JavaScript: where stateful regex is a feature, not a bug.- Timezone drift in staleness detection —
fetchedAtstored in local time, compared against UTC. Off by your timezone offset. All timestamps now usegmdate()consistently, andstrtotime()explicitly appends' UTC'so rogue plugins changing PHP's default timezone can't break staleness checks. - Future timestamps made rates permanently fresh — a future
fetched_at(clock skew, data corruption) produced a negative age, soisStale()always returned false. Now treats future dates as stale. RoundingMode::Noneshowing 15 decimal places — "None" meant "no rounding at all", which meant raw floating point output. Now rounds to the currency's declared decimal count, because€93.4217000000001is not a price.- Corrupted enum values crashed the site —
CurrencyPosition::from()andRoundingMode::from()throwValueErroron unrecognised values. A corrupted option row would take the entire site down. Now usestryFrom()with safe defaults. - Non-numeric API rates crashed cron — if an exchange rate provider returned
"Infinity"or"NaN",bccomp()would throw aValueError. Now validates withis_numeric()before comparison. - ECB provider returned wrong rates for non-EUR base — if the store's base currency wasn't in ECB's data, rates were returned relative to EUR without rebasing. Every price on the site would be wrong. Now returns empty and logs a warning.
- Infinite rate crashed the projection engine — a rate of
Infinitypassed the guard (!Infinityisfalse), rendering every price as"$Infinity". Now usesNumber.isFinite(). - Negative decimals crashed price formatting —
toFixed(-1)throws aRangeErrorthat killed all price projection. Now clamped to 0–20. - Missing JS asset triggered PHP warning —
filemtime()on a deleted file returnedfalsewith a warning. Now suppressed with fallback to the plugin version string. - Refresh lock race condition — between
get_option()andupdate_option(), another process could acquire the lock. Replaced with atomic compare-and-swap. TOCTOU: third time's the charm. - Checkout data changes ignored —
fluentCartCheckoutDataChangedevent (dispatched from 6 places in FluentCart) had no listener. Coupon application, shipping changes, and quantity updates at checkout didn't trigger re-projection. - Price flicker on rapid events — each event handler had its own
setTimeout, so rapid events caused multiple clear+reproject cycles. Now uses a shared debounce timer — last event wins. - Flag emoji crashed without mbstring —
mb_chr()was called without checking if the extension exists. Hosts withoutmbstringgot a fatal error loading the currency catalogue. Now falls back gracefully to empty string, and malformed 1-character country codes no longer cause undefined string offsets. - Cookie persistence ignored the cookie_enabled setting — you could disable cookies in settings and the plugin would cheerfully set one anyway. The setting was decorative, the cookie was eternal.
- Guest currency preference merged on login even with cookies disabled —
mergeGuestPreference()read the cookie regardless of thecookie_enabledsetting. If you disabled cookies, the cookie you already had would still haunt you on next login. - Scientific notation rates crashed BCMath — a rate of
1e3from a provider passesis_numeric()but makesbccomp()throw aValueError. Because PHP thinks1e3is a perfectly reasonable number, and BCMath disagrees. Now rejected at the gate. RoundingMode::Nonerounded instead of truncating — "None" was callinground(), which is... rounding. 33.337 became 33.34 instead of 33.33. Now truncates towards zero, which is what "no rounding" actually means.
Cleanup
- Uninstall now cleans up properly —
fchub_mc_rate_refresh_lockoption andfchub_mc_rl_*rate limiter transients were orphaned on uninstall. Your database was collecting souvenirs. - Uninstall now multisite-aware — single-site installs would clean up fine, but multisite would only hit the main blog. The diagnostics transient and object cache group were also orphaned. Your database was collecting memories.
rounding_precisionsetting removed — was saved, validated, and even sent to the browser, but never consumed by anything. Per-currencydecimalscontrols display precision. Dead weight is dead weight.- Geolocation shown as "Coming Soon" in admin — the resolver exists behind a feature flag, the settings exist in the backend. Now the General tab shows a faded geolocation section so it's visible but clearly not ready yet.
v1.1.6
Released 6 March 2026 — GitHub Release
Three bugs walked into the price projection engine. One was our fault, two were always there.
Regression
- Variant price line hidden by Buy Now button — the v1.1.5 refactor of
replaceInlinePrice()removed explicit whitespace padding around converted prices. ThebasePriceRegexcaptures surrounding whitespace via\s*, andtext.replace(match[0], converted)consumed it without restoring it. The layout collapsed and the Buy Now button swallowed the price line. Now preserves leading and trailing whitespace from the original regex match.
Bugs
- Variant option button prices stuck in base currency — variant buttons render prices in
.fct-product-variant-item-price > spanand.fct-product-variant-compare-price > del > span. These classes aren't in the main price selector list, andprojectVariantButtons()only converted the invisibledata-item-price/data-compare-priceattributes. The visible<span>text stayed in base currency. Now converts both the data attributes and the visible span text. - Subscription variant prices stuck in base currency — subscription products wrap the price text inside a nested
.fct-product-payment-typediv instead of using direct text nodes.projectVariantPrice()only checked direct child text nodes and never found the price. Now descends into the payment type child when no direct text nodes contain a price. Additionally, switching between billing interval tabs (Monthly/Half Yearly/Yearly) caused the converted price to flash then vanish —clearProjectionMarkers()was restoring innerHTML from initial render, which re-applied staleis-hiddenclasses and hid the active tab's price. Now syncs child visibility with the parent's current state after restoration. - Variant compare price blocked actual price conversion — when a variant has both a compare/reference price and an actual price, converting the
<del>compare price incremented the internal counter, which then prevented the code from descending into the payment type child to convert the actual price text node. All one-time tier prices and installment prices stayed in base currency while the strikethrough compare price was correctly converted. Removed the counter guard so both are always converted. - Installment setup fee stuck in base currency — text like "300.00zł per year for 12 cycles + 100.00zł one-time setup fee" contains two prices in one text node.
replaceInlinePrice()only replaced the first match. NewreplaceAllInlinePrices()uses a global regex callback to convert all prices in a single pass, while skipping bare numbers like "12" in "12 cycles". - Min/max price filter sidebar shows unconverted numbers —
projectCurrencySigns()swapped the currency symbol but left the<input>values untouched. A$100filter became€100instead of€93. NewprojectPriceFilterInputs()converts the displayed value while preserving the original base-currency value in a data attribute for re-conversion on slider changes.
v1.1.5
Released 6 March 2026 — GitHub Release
The price projection engine was architecturally deaf. Every dynamic price update — cart changes, variant switches, modal opens — was broken. Hat tip to @ManniGH for the bug reports that exposed the full extent of the carnage.
Critical
- Price projection engine was deaf to FluentCart — all event listeners were attached to
document, but FluentCart dispatches every event onwindow. Custom events don't bubble across event targets. The projection engine never received cart updates, variant changes, or modal opens. Every dynamic price update was broken in production. Fixed by moving all listeners towindow, where FluentCart has been shouting into the void this whole time.
High
- Variant switching left prices in base currency — missing
fluentCartSingleProductVariationChangedlistener. The engine simply didn't know variants existed. Now listens and re-projects with a 200ms delay for FluentCart to finish rendering. - "View Options" modal prices stuck in base currency — missing
fluentCartSingleProductModalOpenedlistener. Same problem, same fix, same facepalm. - Checkout subscription renewal line not converted —
.fct_item_payment_infowasn't in the price selector list. It is now. Subscriptions showing "$9.99/month" next to "€8.72 total" was not the vibe. - Subscription text destroyed during projection — "per month, until cancel" was getting obliterated because the projection replaced entire text nodes. Now uses regex to surgically replace only the price portion, leaving the suffix text intact.
- Pricing table elements destroyed —
<sup>currency signs and<span class="repeat-interval">were being vaporised bytextContentreplacement. Now detects mixed content and modifies only the text node containing the price, because destroying DOM elements is not a currency conversion strategy. fchub_mc_format_price()fatal error —$optionStorewas undefined when the currency context was already cached. Every call after the first page load would crash. The function that formats prices couldn't format prices. Brilliant.
Security
- Rate limiting added to public POST
/context— unauthenticated endpoint could trigger unlimited FluentCRM API calls and DB writes. Now limited to 30 requests/minute per IP. Your store is no longer a free DDoS relay. - Provider names removed from public
/ratesresponse — was leaking which exchange rate API the store uses. Nobody needs to know that. - Checkout disclosure text now sanitised on output — admin-configurable template now runs through
wp_kses()because trusting raw HTML from a settings field is how XSS happens.
Features
- SVG flag icons — bundled 50 SVG country flags from flag-icons (MIT licensed). Windows users no longer see mysterious two-letter codes where flags should be.
- Shortcode
labelattribute —[fchub_currency_switcher label="Select currency:"]renders a text label next to the dropdown. For those who believe in accessibility, or just labelling things. - Shortcode
alignattribute —[fchub_currency_switcher align="right"]supports right and centre alignment. - Per-currency decimal/thousand separators — display currencies can now have explicit separator config instead of the position-based heuristic that thought all right-positioned currencies use commas.
Code Quality
- Resolver chain cached —
buildResolverChain()no longer creates fresh objects on every call. Object creation is not a hobby. - Duplicated
isAllowedCurrency()extracted to trait — was copy-pasted across 4 resolver classes. DRY exists for a reason. - Race condition in stale lock handling fixed —
delete_option+add_optionreplaced with atomicupdate_option. TOCTOU bugs: the gift that keeps on giving. current_time('mysql')replaced — deprecated WordPress function replaced withwp_date()in 2 more files. WordPress has been asking nicely since 5.3.
v1.1.4
Released 6 March 2026 — GitHub Release
Twelve bugs walked into the codebase. None of them survived the code review.
- EUR prices now actually look like EUR prices —
formatNumber()was borrowing the base currency's decimal and thousand separators when formatting display currency prices. EUR showing as1,234.56€instead of1.234,56€. Every right-positioned currency was wrong. Every single one. Fixed. half_downrounding no longer lies — theMath.round(x * (1-EPSILON))trick doesn't hold for all values, as it turns out mathematics hasn't changed. Replaced with a proper floor-based comparison that behaves correctly instead of mostly correctly.fchub_mc_format_price()stops rebuilding the universe on every call — the function was spinning up the full resolver chain plus DB queries each time it was invoked. It now reuses the cached context like a reasonable piece of software.- Stale rate warning stops interrogating the database on every admin page load — now cached via transient with a 5-minute TTL. Your DB server sends its regards.
ExchangeRate::from()no longer throws a fatal on unknown providers — if a provider string in the DB didn't match the enum, you got a crash. It now falls back to Manual and carries on with its life.- Currency switcher
fetch()now handles network failures — previously had no.catch(), so a network error would silently kill the loading spinner forever. Errors are now caught and surfaced like adults. - Rate refresh lock fixed — TOCTOU race condition on
get_option/add_option. Now uses atomicadd_optionfirst, so two concurrent refreshes can't both think they won the lock. - Currency symbol no longer sanitized with
wp_kses_post— allows HTML. A currency symbol field. Tightened tosanitize_text_fieldbecause€is not a<script>tag. current_time('timestamp')replaced withtime()— deprecated across 3 files. WordPress has been politely suggesting this for years.- FluentCrmSync and FluentCommunitySync now go through OptionStore — they were calling settings directly, bypassing the default settings merge. Fixed.
- Rounding precision fallback aligned —
FrontendModulesaid0,Constantssaid2. They now agree on2, which is the number of decimal places a price should have. - Sortable drag-and-drop DOM restoration fixed — the logic for restoring DOM order during downward drags was inverted. Moving a currency down would put it up. Philosophy aside, this is not ideal.
- Admin sidebar
requestAnimationFrameloop bounded — the injection would keep looping forever if FluentCart's markup changed. Now it gives up gracefully instead of melting the browser tab.
v1.1.3
Released 6 March 2026 — GitHub Release
Three bugs walked into a price projector. None of them walked out.
- Strikethrough prices restored — the projection engine was cheerfully nuking
<del>tags when converting compare prices, turning "$100.00$80.00" into just "€74.40" with no visual hint that it used to cost more.findPriceTarget()now recognises<del>elements, so your sale prices actually look like sale prices again. - "From" prices on shop cards now convert — "From $80.00" on product cards was silently ignored because the regex couldn't handle a currency symbol wedged between the prefix text and the digits. Affected every left-positioned currency (USD, GBP, JPY…) — which is, you know, most of them.
- Double-conversion edge case fixed — compare prices inside variant containers could get converted twice when the display currency used a right-positioned symbol (e.g.
93.00€→parseFloatwould happily parse it again). Child compare-price elements are now marked as projected after the parent variant processes them.
v1.1.2
Released 6 March 2026 — GitHub Release
Fixed exchange rate cron going missing after deactivation/reactivation cycles. The refresh event was only scheduled inside register_activation_hook — if WordPress cleared it for any reason, rates would go stale with no way to recover. The init hook now re-schedules the cron automatically when it detects it's missing.
v1.1.1
Released 6 March 2026 — GitHub Release
Fixed a fatal error when multiple FCHub plugins are active at the same time. The shared GitHubUpdater class used a class_exists guard that PHP's OPcache cheerfully ignored during early class binding — so the second plugin to load would redeclare the class and take the site down. Wrapped the class inside the conditional so OPcache actually respects it.
v1.1.0
Released March 2026 — Currency reorder
- Drag-and-drop currency ordering — reorder currencies in the Currencies tab with drag handles. The order you set is the order visitors see in the currency switcher dropdown. Powered by SortableJS.
v1.0.0
Released March 2026 — Initial release
The first release of FCHub Multi-Currency. Display-layer multi-currency for FluentCart — because not everyone thinks in dollars.



Core Features
- Currency switcher widget — custom dropdown with flag emojis, ARIA listbox roles, keyboard navigation, and a rate freshness badge. Place it anywhere with
[fchub_currency_switcher] - Price projection engine — JavaScript-based real-time price conversion across all FluentCart elements. Product cards, cart drawer, checkout totals, pricing tables, variant buttons — everything gets projected
- Exchange rate management — four providers (Exchange Rate API, Open Exchange Rates, European Central Bank, Manual), automatic cron refresh, rate history with 90-day retention, staleness detection
- Checkout disclosure — configurable notice at checkout with template tokens (
{base_currency},{display_currency},{rate}). Injected into checkout summary, cart drawer, and cart page - Order snapshots — display currency, base currency, and exchange rate saved to order metadata on payment
- Context resolution chain — URL parameter → user meta → cookie → geolocation (feature-flagged) → fallback default. First match wins, all validated against display currency whitelist
Integrations
- FluentCRM sync — automatic contact tagging with currency tags, custom field updates on context switch and order payment
- FluentCommunity sync — preferred currency written to community user meta
- FluentCart addon — appears in FluentCart's integration modules, extends store settings API, injects checkout data fragments
Admin
- Six-tab settings SPA — General, Currencies, Exchange Rates, Checkout, CRM, and Diagnostics. Built as a Vue 3 component inside FluentCart's admin
- Real-time diagnostics — plugin version, PHP/bcmath status, FluentCart/FluentCRM presence, rate count, stale currency detection, feature flags
- Stale rate admin notice — dashboard warning when any exchange rate exceeds the stale threshold
Developer
- REST API — public endpoints for context get/set and rate listing; admin endpoints for settings, rate refresh, currency catalogue, and diagnostics
- PHP API —
fchub_mc_format_price()function for theme/plugin developers - JavaScript API —
window.fchubMcSwitchCurrency(),window.fchubMcInitSwitchers(),window.fchubMcProjectPrices()globals - Custom hooks —
fchub_mc/context_switched,fchub_mc/rates_refreshed,fchub_mc/contextfilter,fchub_mc/modulesfilter - GDPR compliant — personal data exporter and eraser registered with WordPress privacy tools
- Feature flags —
js_projectionandgeo_resolverfor gradual rollout control