Descrição
Saga Payments for WooCommerce is a full-featured payment gateway built for Nordic merchants. Accept secure payments through multiple payment methods with enterprise-grade features:
Payment Methods
- Card Payments – Visa, Mastercard, American Express, Discover
- Vipps – Norway’s most popular mobile payment app
- Klarna – Buy now, pay later
- Apple Pay – Fast checkout for Apple users
- Google Pay – Fast checkout for Android users
- Swish – Sweden’s most popular mobile payment app
- MobilePay – Denmark and Finland’s popular mobile payment app
Features
- Easy Setup – Just enter your Merchant ID, Terminal ID, and Public Key
- Saved Payment Methods – Customers can save cards for faster checkout
- WooCommerce Subscriptions – Full support for recurring payments with automatic renewals
- WooCommerce Pre-Orders – Charge customers when pre-ordered products become available
- Authorize & Capture – Authorize payments and capture later, or capture immediately
- Auto-Capture – Automatically capture when order status changes to Processing/Completed
- Auto-Void – Automatically void authorizations when orders are cancelled
- Express Checkout – Apple Pay/Google Pay buttons on product pages and cart
- WooCommerce Blocks – Full support for Blocks Checkout including saved cards
- HPOS Compatible – Full support for High-Performance Order Storage
- Refunds – Full and partial refunds directly from WooCommerce admin
- Professional Design – Clean, responsive payment widget
Plugin Integrations
- WooCommerce Subscriptions – Automatic recurring payments
- WooCommerce Pre-Orders – Charge on release
- WooCommerce Blocks – Full Blocks checkout support
- WooCommerce HPOS – High-Performance Order Storage
- Saga Product Sync – Bidirectional product catalogue synchronisation
- Compatible with all major WordPress themes
Product Sync (Saga)
Automatic bidirectional synchronisation between your WooCommerce store and Saga:
- Saga is master – pull products, prices, images, VAT rates and stock from Saga into WooCommerce
- Push changes back – WooCommerce product edits are automatically pushed to Saga in real-time
- Inventory sync – relative stock deltas (STOCK_DOWN / STOCK_UP) on order completion, refund and cancellation
- Auto-retry – failed inventory adjustments are queued and retried every 5 minutes (up to 20 attempts)
- Full field mapping – prices (øre kroner), VAT rates (25 / 15 / 12 / 0 %), units, barcodes, cost price, images
- Admin panel – WooCommerce -> Saga Product Sync with connection test, manual sync, and status dashboard
- Configure via WooCommerce -> Saga Product Sync
Requirements
- WordPress 5.8 or later
- WooCommerce 7.0 or later
- PHP 7.4 or later
- SSL certificate (HTTPS)
- Saga Payments merchant account
External services
This plugin relies on the following external services to process payments. A Saga Payments merchant account is required.
Saga Payments API
All payment operations (order creation, payment verification, refunds, captures, voids, and subscription management) are handled through the Saga Payments API proxy server. When a customer initiates a checkout, order data including amount, currency, line items, and customer billing/shipping information is sent to this service via server-side HTTP requests.
- Service URL: https://sagapay-api-v3.kristoffer-afc.workers.dev/api
- Provider: Saga Payments (Cloudflare Worker proxy to Surfboard Payments)
- Website: https://sagapay.no
- Terms of Service: https://www.sagapay.no/terms-of-service
- Privacy Policy: https://www.sagapay.no/personvernserklaering
Saga Payments Merchant Dashboard and Documentation
The plugin settings screen links store administrators to Saga Payments dashboard and documentation pages for API key, webhook, and product catalogue setup. These links open only when an administrator clicks them. No customer or payment data is sent automatically by these documentation links.
- Dashboard URL: https://dashboard.sagapay.no
- API key documentation URL: https://docs.sagapay.no/guides/getting-started/opprett-api-nokler
- Webhook documentation URL: https://docs.sagapay.no/guides/getting-started/webhooks
- Product catalogue documentation URL: https://docs.sagapay.no/api-reference/api-reference/product-catalogue/
- Provider: Saga Payments
- Terms of Service: https://www.sagapay.no/terms-of-service
- Privacy Policy: https://www.sagapay.no/personvernserklaering
Saga OTP Verification Service
When a guest customer pays with a previously saved card, the plugin sends a one-time password (OTP) verification email via a Cloudflare Worker. Only the customer’s email address, a generated OTP code, the store name, and the cart total are sent in the request. No card data is transmitted. The OTP email is delivered via the worker using the Resend email service.
- Service URL: https://saga-otp-worker.kristoffer-afc.workers.dev
- Provider: Saga Payments (Cloudflare Worker)
- Website: https://sagapay.no
- Terms of Service: https://www.sagapay.no/terms-of-service
- Privacy Policy: https://www.sagapay.no/personvernserklaering
Surfboard Online SDK
The payment form displayed on your checkout page is rendered by the Surfboard Online SDK. This JavaScript library is loaded from an external server into the customer’s browser and handles the secure payment UI including card input fields, Vipps/MobilePay popups, Klarna widget, and Apple Pay / Google Pay buttons. Card data is entered directly into Surfboard-hosted iframes and never touches your server.
- SDK URL: https://thorium.surfgw.com/OnlineSDK.js
- Provider: Surfboard Payments
- Website: https://www.surfboardpayments.com
- Terms of Service: https://www.surfboardpayments.com/terms-and-conditions
- Privacy Policy: https://www.surfboardpayments.com/privacy-policy
Surfboard Hosted Payment Page
When the plugin is configured in redirect mode, or for certain subscription flows, the customer’s browser is redirected to a hosted payment page operated by Surfboard Payments. The order amount, currency, and return URLs are passed via the redirect. All payment data is entered on the hosted page.
- Service URL: https://pay.withsurfboard.com
- Base URL used by redirects: https://pay.withsurfboard.com/
- Provider: Surfboard Payments
- Terms of Service: https://www.surfboardpayments.com/terms-and-conditions
- Privacy Policy: https://www.surfboardpayments.com/privacy-policy
Surfboard Vipps/MobilePay Intermediary
For Vipps and MobilePay payment flows, the Surfboard SDK loads an intermediary page in an iframe to handle the mobile payment authorization. This is managed automatically by the Surfboard Online SDK and serves as the bridge between your store and the Vipps/MobilePay apps.
- Service URL: https://vipps.withsurfboard.com
- Provider: Surfboard Payments
- Terms of Service: https://www.surfboardpayments.com/terms-and-conditions
- Privacy Policy: https://www.surfboardpayments.com/privacy-policy
Google Pay API
When Google Pay is enabled as a payment method, the Google Pay JavaScript SDK is loaded in the customer’s browser to render the Google Pay button and handle the payment sheet. Payment token data is returned to your store and forwarded to the Saga Payments API for processing.
- SDK URL: https://pay.google.com/gp/p/js/pay.js
- Provider: Google
- Website: https://pay.google.com
- Terms of Service: https://payments.google.com/payments/apis-secure/get_legal_document?ldo=0&ldt=googlepaytos
- Privacy Policy: https://policies.google.com/privacy
QR Code Generation
For Swish payments, if the payment gateway does not return a pre-rendered QR code image, the plugin generates a QR code locally using the bundled qrcode-generator library (MIT license, by Kazuhiko Arase). No external service is called for QR code generation.
Saga Checkout SDK (Diagnostics Only)
The plugin diagnostics page displays the configured SDK endpoint for admin troubleshooting purposes. The actual payment SDK loaded during checkout is the Surfboard Online SDK documented above.
- Provider: Saga Payments
- Website: https://sagapay.no
- Terms of Service: https://www.sagapay.no/terms-of-service
- Privacy Policy: https://www.sagapay.no/personvernserklaering
Saga Product Sync API
If the optional product synchronization feature is enabled, the plugin sends product data (name, description, price, SKU, images, categories) from your WooCommerce catalog to the Saga Payments product API for catalogue management. This is an opt-in feature configured in the plugin settings.
- Service URL: https://api.sagapay.no
- Provider: Saga Payments
- Terms of Service: https://www.sagapay.no/terms-of-service
- Privacy Policy: https://www.sagapay.no/personvernserklaering
Payment Method Logos
Payment method logos (Visa, Mastercard, Amex, Vipps, Klarna, Apple Pay, Google Pay, Swish, MobilePay) are bundled locally within the plugin in SVG format. No external CDN is used for logo display.
SVG files and inline SVG markup use the standard SVG namespace URI http://www.w3.org/2000/svg and xlink namespace URI http://www.w3.org/1999/xlink as static XML identifiers; these are not external network requests.
Instalação
- Upload the
saga-paymentsfolder to the/wp-content/plugins/directory - Activate the plugin through the ‘Plugins’ menu in WordPress
- Go to WooCommerce > Settings > Payments > Saga Payments
- Enter your Merchant ID and Store ID (from your Saga Payments dashboard at https://dashboard.sagapay.no) and click Save changes
- The plugin will automatically provision your Terminal ID and Public Key on save – no manual key handling required
- Enable the payment method and save
FAQ
-
How do I get a Saga Payments account?
-
Contact Saga Payments at support@sagapayments.com to set up your merchant account.
-
Does this support test mode?
-
Yes! Enable test mode in the plugin settings to test without processing real payments.
-
Which currencies are supported?
-
The plugin supports NOK (Norwegian Krone), SEK (Swedish Krona), DKK (Danish Krone), EUR and other currencies supported by Saga Payments.
-
Can customers save their cards?
-
Yes! When “Enable Saved Cards” is turned on in settings, logged-in customers can save their cards for faster future checkout.
-
Does this work with WooCommerce Subscriptions?
-
Yes! Full integration with WooCommerce Subscriptions for automatic recurring payments, payment method changes, and subscription lifecycle management.
-
Yes! Set “Capture Mode” to “Manual” in settings. Payments will be authorized at checkout and you can capture from the order page when ready to ship.
Avaliações
There are no reviews for this plugin.
Contribuidores e desenvolvedores
“Saga Payments for WooCommerce” é um software com código aberto. As seguintes pessoas contribuíram para este plugin.
ContribuidoresTraduzir “Saga Payments for WooCommerce” para o seu idioma.
Interessado no desenvolvimento?
Navegue pelo código, dê uma olhada no repositório SVN ou assine o registro de desenvolvimento via RSS.
Registro de alterações
4.0.420
- Product sync: stop calling the
/product-catalogue/{catId}/productsFetch Products variant without a productId query, which was raisingPC_0061: Missing minimum paramter to fetch producton SagaPay’s PC_GET_PRODUCTS alerting. Only the documented/product-catalogue/{catId}?storeId=...endpoint is used to list products now.
4.0.419
- Product pull now enables WooCommerce stock tracking for tangible Saga catalogue products even when Saga’s fetch response omits inventory values, preserving a readable Woo stock baseline or using an in-stock baseline so Woo does not leave managed stock disabled.
4.0.418
- Product pull now activates WooCommerce stock tracking for existing Saga-managed products that already have a Woo stock value when the Saga API omits an absolute inventory quantity, preserving that value instead of leaving tracking disabled.
4.0.417
- Product pull now treats Surfboard inventory payloads as a stock-management signal even when the API omits a readable quantity, enabling WooCommerce stock tracking without overwriting existing stock with zero.
4.0.416
- Product statistics fallback now includes the connected
storeId, so Surfboard stock can be imported when the product payload omits inventory fields.
4.0.415
- Product pull now guarantees WooCommerce
_manage_stock,_stock,_stock_statusand_saga_synced_stockmeta are persisted after Surfboard stock quantities are imported.
4.0.414
- Surfboard stock quantities from
inventoryStatus.currentStocknow activate WooCommerce stock management and write the remote stock quantity during product pull.
4.0.413
- STRICT CONNECTED STORE PRODUCT SYNC (wave10es) – Product sync is scoped back to the connected Store ID only. Merchant-wide store discovery is not used for Woo catalogue import, SKU dedupe, cart authority checks or inventory retry pruning.
4.0.412
- STORE-AWARE PRODUCT WRITE PATHS (wave10er) – Products imported from another active Surfboard store now carry their owning store through related-products, variable/variant sync, stock updates, order/refund/cancel inventory safety nets and inventory retry queue entries. This prevents later Woo actions from writing back to the gateway store when the product belongs to another Surfboard store.
4.0.411
- ALL-STORE CATALOG PULL (wave10eq) – Product pull now discovers active stores under the merchant via the live
/stores?merchantId=...API and reads each store product catalogue, not only the gateway store. Imported products remember_saga_store_idso later delete/update operations can target the store that actually owns the Surfboard product.
4.0.410
- MAIN DESCRIPTION AUTHORITY (wave10ep) – WooCommerce full product description is now the authoritative source for the Saga top-level
descriptionfield on push. Short description is still preserved separately inproductProperties.shortDescription, so editing the main Woo description no longer leaves Surfboard stuck with an old short description.
4.0.409
- NATIVE GTIN PUSH FIX (wave10eo) – WooCommerce’s native GTIN/EAN field is now read via
get_global_unique_id()before fallback meta keys, and product admin saves run one final push after Woo has persisted product meta. This closes the live-tested gap where name, price and stock reached Surfboard immediately but a changed Woo GTIN could remain stale remotely until overwritten by the next pull.
4.0.408
- LIVE BARCODE PAYLOAD FIX (wave10en) – Live Surfboard persists EAN/barcode updates from the lowercase
barcodefield, while the docs showbarCode. Product and variant push payloads now send both field names so Woo-created or Woo-updated products keep EAN/GTIN in Surfboard and pull back into Woo’s native GTIN field.
4.0.407
- PRODUCT FIELD AUTHORITY (wave10em) – Pull now fetches each full Surfboard product by ID before mapping, so compact list responses cannot drop description, category, barcode/EAN/GTIN, HSN, tax, product properties, images or other documented fields. Saga categories are now applied as real WooCommerce product categories, barcode/EAN is written through Woo’s native GTIN setter when available and common EAN plugin meta keys are covered.
- EMPTY SURFBOARD CATALOGUE AUTHORITY – If Surfboard/Saga returns zero products while Woo has active products, the pull treats the empty catalogue as authoritative and moves active Woo products out of the live catalogue instead of pushing them back up and resurrecting POS-deleted products.
- VARIANT AND TAX HARDENING – Saga variants pulled from Surfboard are materialised as Woo variable product variations with attributes, variant IDs, stock, EAN/GTIN, HSN and raw variant payload metadata. Woo product updates now send the tax array as well as the existing VAT field, with a fallback retry that strips tax if an endpoint rejects tax on PATCH.
4.0.406
- STALE INVENTORY RETRY PRUNE (wave10el) – Inventory retry queue entries are now removed when a linked Saga product is deleted or orphan-cleaned from WooCommerce, and retry cron drops stale adjustments when the current Saga catalogue no longer contains the product ID. This prevents old STOCK_UP/STOCK_DOWN retries for deleted products from retrying for 24 hours and sending manual correction emails after the product is already gone upstream.
4.0.405
- CART/CATALOG AUTHORITY PRODUCT SYNC (wave10ek) – Existing WooCommerce cart sessions are now swept on cart load and checkout validation so Saga-managed products that were deleted upstream or removed from the publishable Woo catalogue cannot remain purchasable from stale sessions. Add-to-cart now verifies linked Saga products against the current catalogue before accepting the item.
- MULTI-CATALOG PRODUCT ROUTING – Product pulls now parse live plural catalogue fields such as
productCatalogueIds, store_saga_catalogue_idon Woo products/variations, report all catalogue counts in Test Connection, and use the owning catalogue ID for product updates, deletes, related-products, variants, product statistics, inventory deltas and retry-queue replays.
4.0.404
- WOO DELETE/STATUS PRODUCT SYNC (wave10ej) – WooCommerce product trash/delete and publish-status changes now propagate to Surfboard/Saga by deleting the linked catalogue product when the Woo product leaves the publishable catalogue. Draft/private/pending products are no longer pushed into Surfboard from save hooks, and successful remote deletes clear the active
_saga_product_idwhile keeping audit meta so a later restore/publish creates a clean new catalogue product instead of leaving POS with stale inventory.
4.0.403
- DELETE-SAFE MANUAL PRODUCT SYNC (wave10ei) – Manual Sync no longer pushes Woo products back into Surfboard/Saga in the same run after pull-side cleanup removed POS-deleted products from WooCommerce. This prevents remote deletions from being immediately resurrected by the push phase when the Surfboard catalogue contains fewer products than Woo.
4.0.402
- AUTHORITATIVE PRODUCT SYNC MIRROR (wave10eh) – Treats an empty Surfboard/Saga catalogue as an authoritative delete state after a previous sync or when Saga-managed products exist in WooCommerce. Manual Sync now refuses to push Woo products back into Saga immediately after an authoritative empty pull, so POS-side product deletion cannot be undone by the plugin. The pull cleanup can remove all active Woo products from the catalogue in this state, not only products still carrying a current
_saga_product_id. - DOCUMENTED SURFBOARD PRODUCT ENDPOINTS – Product catalogue API calls now try the documented
/catalog,/catalog/:catalogId/products, product, variant, related-products, statistics and inventory routes first, while keeping the older/product-catalogueroutes as fallback. The catalogue ID cache namespace was refreshed so stores do not stay pinned to an old/parallel catalogue. - INVENTORY SYNC HARDENING – Product stock parsing now accepts
inventory.currentStock,stock,availableQuantity,inventoryQuantityand related Surfboard field shapes, and falls back to product statistics when the product list omits stock. Order completion, refund and cancellation safety nets now resolve variation inventory bindings and queue variant retry adjustments instead of only handling simple product stock.
4.0.401
- WALLET GHOST-TRIGGER GUARD (wave10eg) – Prevents Apple Pay / Google Pay from being launched again after the customer closes the wallet sheet. Classic checkout now treats wallet cancel/reset as a durable stale-attempt signal, blocks checkout-success and payment fallback paths from calling
initiatePayments()without a fresh trusted Place Order click, and skips automatic SDK remount immediately after a clean wallet cancellation.
4.0.400
- CLEAN CHECKOUT UTF-8 FALLBACKS (wave10ef) – Fixes mojibake in customer-facing classic checkout fallback messages, including the Apple Pay / Google Pay cancelled notice that could render as
Prøv igjeninstead ofPrøv igjen. Classic checkout now normalizes known double-encoded fallback text before display and routes wallet cancel notices through the central i18n keys.
4.0.399
- ORDER ATTRIBUTION PAYMENT-DOMAIN GUARD (wave10ee) – Prevents WooCommerce Order Attribution from recording Saga/Surfboard payment domains such as
pay.withsurfboard.comas the merchant’s traffic source after redirect/cancel/retry flows. Classic checkout now snapshots clean Woo attribution before Saga submission and restores it before order creation if the browser came back from the payment page; the server also removes or restores polluted_wc_order_attribution_*meta for classic, Blocks and programmatic Saga orders.
4.0.398
- PRODUCT PULL REACTIVATION FIX (wave10ed) – Fixes the case where a Surfboard/Saga product is found during pull and the sync reports it as updated, but WooCommerce still does not show it because the matched Woo product stayed as draft/private/pending. Pull updates now publish and make visible any product returned by the Saga catalogue, because Saga is the pull-side source of truth.
- RESTORE LINKED PRODUCTS FROM TRASH – If Saga returns a product whose
_saga_product_idalready exists in Woo trash, the pull lookup now finds it and restores/publishes it instead of creating another hidden/duplicate copy.
4.0.397
- PRODUCT VARIANT ENDPOINT COVERAGE (wave10ec) – WooCommerce variable products are no longer skipped by product sync. Variable parent products are created/updated as Saga catalogue products, Woo variations are created/updated through the Saga variant endpoints, variation IDs are stored on each WC variation, and variation stock changes now use the Saga variant inventory endpoint instead of being ignored as unlinked stock changes. Variation save hooks are registered so admin edits to size/color variants push immediately.
- DOCUMENTED PRODUCT ROUTE/VERB FALLBACKS – Product API calls now try the documented plural
/products/:productIdroute shape and documentedPATCHinventory methods first, while keeping the older singular/product/:productIdand legacyPUTinventory calls as quiet fallbacks for existing deployments. This covers create/fetch/update/delete, related products, add/update variants, product inventory, variant inventory, and product statistics without breaking stores still served by the previous route aliases. - VARIABLE PRODUCT FIELD COVERAGE – Product mapping now derives parent prices from the minimum variation price when a variable parent has no direct regular price, and sends variant category metadata from Woo variation attributes so Surfboard/POS receives the variant dimensions needed to group child variants correctly.
4.0.396
- PRODUCT SYNC EMPTY-CATALOG ORPHAN FIX (wave10eb) – Fixes the exact case where Surfboard/Saga has zero products but WooCommerce still shows old products after manual sync. The pull orphan reconciliation now runs even when the upstream product list is empty; every active Woo product linked with
_saga_product_idthat is missing upstream is moved to Woo trash (not permanently deleted, so historical orders stay safe) and tagged with_saga_orphaned_at,_saga_orphaned_saga_id, and_saga_orphan_actionfor auditability. Manual sync now reports how many products were removed from the active Woo catalogue. - PRODUCT FIELD/API COVERAGE – Extends product mapping to preserve
hsnCode, unit type, product type, related products, variant categories, variants, billing/monthly plans, and campaign metadata, while continuing to sync barcode/EAN/GTIN, price, cost price, VAT, unit, images, dimensions, weight, stock and product properties. Adds catalogue API wrappers for fetch-by-id, related products, add/update variants, variant inventory, catalogue statistics and product statistics, and includesstoreIdin product catalogue request bodies/queries where required by the Surfboard product endpoint documentation. - RELATED PRODUCTS SYNC – Saga
relatedProductsnow pull into Woo upsells after all products are linked, and Woo upsell/cross-sell relationships are pushed back to Saga related products when related products already have Saga IDs. - PRODUCT SYNC INVARIANTS – Adds a dedicated product-sync invariant test that locks the empty-catalog cleanup, related-products mapping, required product API wrappers and field round-trip coverage so this regression cannot silently come back.
4.0.395
- CAPTURE/VOID ALLOWLIST (wave10ea) – Manual Capture and Void buttons (and the matching WooCommerce order-actions, auto-capture-on-Processing/Completed and auto-void-on-Cancelled hooks, plus their AJAX handlers) are now restricted to payment methods Saga actually exposes an authorise/capture/void lifecycle for: Card, Apple Pay and Google Pay. All other methods (Klarna, Vipps, Swish, MobilePay, etc.) are auto-captured by the upstream provider on authorisation — there is no separate authorisation that can be voided, so the previous Void button silently failed and never returned funds to the customer. For those orders the Saga Payment Actions meta box now shows a clear notice telling the merchant to use the WooCommerce Refund button instead, and the order-actions dropdown no longer offers Capture or Void. Method is resolved from
_saga_payment_method_used(API-confirmed) with fallback to_saga_payment_method(checkout-time selection); REDIRECT placeholders are excluded.
4.0.394
- WALLET METHOD-SWITCH GUARD (wave10dz) – Adds a final synchronous DOM re-check immediately before calling
SDK.order.initiatePaymentsin the wallet click handler. Reads the live#saga_payment_methodhidden input value and aborts cleanly (no popup, no state mutation) if the customer has switched to a non-wallet method (CARD, Klarna, Vipps, Swish, MobilePay) in the same event tick. Prevents a rare scenario where users who switch from Google Pay or Apple Pay to Card payment could see a stale Google Pay popup appear from a click that had already been queued in the wallet handler. Wallet-only scope preserved — non-wallet flows are unchanged because the wallet click handler is attached only for GP/AP and this re-check only runs inside that handler.
4.0.393
- MANUAL CANCEL ON “STARTER BETALING…” OVERLAY (wave10dy) – If the customer closes the Google Pay or Apple Pay popup (or the popup otherwise fails to complete), the “Starter betaling… Vennligst vent mens betalingsvinduet åpnes” overlay no longer leaves them permanently stuck. An “Avbryt betaling” button now appears on that overlay after a 2.5-second delay (delay prevents the button from flashing during a normal fast popup open). Clicking it calls
resetPaymentForRetrywhich stops all wallet/payment monitoring, clears in-flight flags, and lets the user pick another method or retry. Mechanism: addedcancelDelayMsoption toshowProcessingOverlayso a cancel button can be shown after a configurable delay; the “Starter betaling…” call now passes{ cancel: true, cancelDelayMs: 2500 }instead of the previous{ cancel: false }. The “Behandler bestillingen…” overlay is unchanged (still no cancel) because that runs before the SDK is even involved.
4.0.392
- GOOGLE PAY / APPLE PAY — CLEAN ONE-CLICK CHECKOUT (wave10dx, wallet path only) – Removes the yellow/green “klikk Fullfør bestilling på nytt” notice introduced in 4.0.391. Replaces it with a silent button-readiness gate: when Google Pay or Apple Pay is the selected method but
SurfboardOnlineSDK.order.initiatePaymentsis not yet callable, the Place Order button is briefly faded (pointer-events:none + opacity 0.6) so the user physically cannot click before the SDK is ready. The gate polls every 120 ms and releases the instant the SDK is callable, so the user’s very first real click opens the wallet popup synchronously from a trusted gesture (no Firefox popup block, no “click again” prompt). Scope is strictly wallet-only — CARD, Klarna, Vipps, Swish, MobilePay are untouched because the gate is engaged only insideattachWalletClickHandlerfor GP/AP. If a click somehow lands during the unready window it is now blocked silently (no inline notice, justpreventDefault+stopImmediatePropagation+ console.warn diagnostic) and the gate engages so the next click works.
4.0.391
- GOOGLE PAY / APPLE PAY “POPUP NEVER OPENS” FIX (wave10dw, wallet path only) – On classic checkout in Firefox (and any browser with strict popup-blocking under non-user-gesture context), the Place Order button could be clicked during the brief window where
updated_checkouthad torn down and was re-mounting the Saga SDK. The wallet click handler sawSurfboardOnlineSDK.order.initiatePaymentswas not yet callable, silently bailed, and the click fell through to WooCommerce’s asynchronous form submit. By the time the WC AJAX returned and tried to open the wallet popup, the user gesture was gone and Firefox blocked the window. The bug presented as the “Starter betaling\u2026” overlay showing forever with no Google Pay popup appearing. This release replaces the silent bail with a synchronous block (preventDefault+stopImmediatePropagation), polls SDK readiness for up to 5 seconds, and surfaces an inline message asking the user to click Fullf\u00f8r bestilling again. The next click is a fresh trusted gesture so the popup opens immediately. Scope is strictly wallet-only (Google Pay + Apple Pay); CARD, Klarna, Vipps, Swish, and MobilePay flows are unchanged because the handler is only attached for wallet methods. Always-on console.warn diagnostics added so the bail path is visible without enabling?saga_debug=1.
4.0.390
- Verification release – bumps the version so that merchants on 4.0.389 see an update available on the Updates screen. With the 4.0.389 root-cause fix in place (plugin header stripped from
saga-payments-gateway.php, active_plugins migration moved to canonical), the Updates screen should now show exactly one Saga Payments row and never the duplicate that crashed the bulk upgrader in earlier releases. No functional code changes in this release.
4.0.389
- DUPLICATE PLUGIN ROW ROOT-CAUSE FIX – Removes the WordPress plugin header (Plugin Name, Version, Description, etc.) from
saga-payments-gateway.phpso thatget_plugins()no longer indexes it as a separate plugin entry. The duplicate row appeared because WordPress’s plugin discovery scans every .php file with aPlugin Name:header and treats each match as a distinct plugin; we historically shipped two files with plugin headers in the same folder (the canonicalsaga-payments.phpand the legacysaga-payments-gateway.phpkept for backward compatibility with installations whoseactive_pluginsoption still recorded the legacy basename). Now the legacy file is a header-less compatibility loader that only doesrequire_once __DIR__ . '/saga-payments.php';so backward compatibility for un-migrated stores is preserved (WordPress validates active_plugins entries viafile_exists, not via the plugin header, so the legacy basename still loads correctly). Theactive_pluginsmigration code that used to live in the legacy file has moved to canonical so it runs on every boot regardless of which entry point WordPress picked. The 4.0.387 / 4.0.388 runtime filters (all_plugins, site_transient_update_plugins, CSS, JS) remain in place as belt-and-suspenders defense but are no longer the primary mechanism.
4.0.388
- UPDATES SCREEN DUPLICATE ROW + BULK-UPDATE CRASH FIX – The 4.0.387 dedupe handled wp-admin Plugins list but did NOT touch the Updates screen (
update-core.php), which reads from a separate WordPress transient (update_plugins) containing both basenames. When merchants ticked both rows and clicked Update, WordPress tried to upgrade the samesaga-payments/folder twice and the second pass hit a half-written ZIP, crashing the upgrader. This release filters thesite_transient_update_pluginsandtransient_update_pluginsread paths at PHP_INT_MAX priority to scrub the legacysaga-payments/saga-payments-gateway.phpentry fromresponse,no_update, andcheckedbuckets. A matching CSS+JS row-removal runs onadmin_head-update-core.phpso any stale-cached transient still renders as a single row. A one-shotdelete_site_transient('update_plugins')on first 4.0.388 boot forces a fresh wp.org check to seed the now-filtered cache cleanly.
4.0.387
- PLUGINS LIST DUPLICATE ROW BULLETPROOF FIX – The 4.0.386 PHP
all_pluginsfilter did not take effect on all sites (suspected causes: opcache serving stale bootstrap code after in-place update, third-party plugin re-adding the entry at a later filter priority, or WP_List_Table population paths that bypass the filter). This release adds two reinforcements: (a) theall_pluginsfilter now runs at PHP_INT_MAX priority so no other filter can revive the legacy row, and (b) a server-rendered CSS rule injected onadmin_head-plugins.phphides the<tr data-plugin="saga-payments/saga-payments-gateway.php">row directly in the rendered table withdisplay:none !important. The CSS path works even if opcache is serving stale PHP, because the new code is what writes the style tag in the first place. After updating, deactivate and reactivate Saga Payments once to force PHP-FPM to pick up the new bootstrap if opcache.revalidate_freq is high.
4.0.386
- PLUGINS LIST DUPLICATE ROW FIX – The legacy
saga-payments-gateway.phpcompatibility entry-point no longer shows as a second “Saga Payments for WooCommerce” row on the wp-admin Plugins list. The 4.0.319 hide-filter that was supposed to suppress it was bailing out whenever the plugins.php URL carried anyaction/action2query parameter (added in 4.0.329 to protect upload-conflict detection), but WP’s upload-conflict flow runs on update.php, not plugins.php, so the action-query guard was unnecessary and was causing the duplicate row to render whenever WordPress redirected back to plugins.php with an action parameter (e.g. after an activation, or when a host or security plugin added tracking params). The filter now hides the legacy basename unconditionally on the plugins list render, while still allowing it through on update.php, plugin-install.php, AJAX, REST and WP-CLI so slug-collision / Replace-current upload flows continue to work.
4.0.385
- EXPRESS SWISH OPEN-APP BUTTON RESTORED – The express-checkout quickbuy popups (product, cart, checkout) now consistently render the “Åpne Swish-appen” button inside the Swish QR state on both mobile and desktop, matching the classic-checkout and Blocks behaviour added in 4.0.381 and 4.0.383. Two fixes were needed: (1) the express mobile branch no longer auto-redirects to swish:// before the user sees the modal – the QR modal with the Open-app button is now always shown when the SDK returns qrData, and the user’s tap on the button is the fresh user gesture iOS Safari requires for custom-scheme navigation; (2) a string-only deep scan of the SDK result was added as a third fallback for the swish:// URL (after the well-known getters/property keys and the qrData fallback), mirroring the helper added to checkout.js / blocks.js so the button still renders when the SDK tucks the deep link into a nested or non-standard field. Auto-redirect only kicks in now as fallback when the SDK did not return a QR payload at all.
4.0.384
- SWISH CLASSIC MOBILE FALL-THROUGH FIX – The classic-checkout mobile Swish branch no longer early-returns after mounting the QR modal and no longer raises a hard “Kunne ikke åpne Swish-appen. Prøv igjen” error when neither qrData nor a usable swish:// URL was returned by the SDK. Control now falls through to the natural post-initiatePayments polling setup just like the desktop branch did all along, so the modal is rendered AND the backend payment-status polling is armed in lockstep. If the SDK returns nothing at all the normal fall-through handles the verify_url redirect path without a duplicate error overlay.
4.0.383
- SWISH QR MODAL ALWAYS SHOWN – Classic checkout and WooCommerce Blocks now always render the Swish QR code modal with the “Åpne Swish-appen” button on BOTH mobile and desktop instead of auto-redirecting to the swish:// app on mobile before the modal mounts. Customers now consistently see the QR code (so a second device can scan it) and an explicit button to launch the Swish app on the current device. The user’s tap on that button is its own fresh user gesture, so iOS Safari still accepts the subsequent swish:// navigation – the v4.0.380 user-gesture concern is preserved without hiding the visual QR confirmation. Auto-redirect only kicks in as fallback when the SDK did not return a QR payload at all.
4.0.382
- SWISH DEEP-SCAN SIDE-EFFECT GUARD – Removed the SDK getter invocations from the 4.0.381 deep-scan helper. Generic getter names such as getRedirectUrl could trigger SDK side effects (auto-navigation toward the Saga payment page) on certain merchant configurations, breaking the Swish flow entirely – the checkout would briefly show “Betalingssystemet laster, vent litt” and then auto-navigate to the Saga verify_url page instead of opening the Swish app or rendering the QR modal. The deep scan now only walks string properties of the SDK result for a usable swish:// or intent:// URL and never invokes additional getters. The regular code path still calls the safe Swish-specific getters before deep scan kicks in.
4.0.381
- SWISH OPEN-APP BUTTON DEEP SCAN – Classic checkout and WooCommerce Blocks Swish QR modals now find the swish:// deep link even when the SDK tucks it into a nested or non-standard field. The mobile redirect path, the desktop QR modal, and the blocks modal all fall back to a recursive deep scan of the entire initiatePayments result for a usable swish:// / intent:// URL after the well-known getters/property keys and the qrData fallback have been exhausted. This fixes the case where 4.0.380 surfaced the QR code on classic checkout and blocks but no Open Swish app button rendered because the SDK on the merchant’s terminal returned the QR payload as a data:image/png blob and exposed the deep link only via a nested field.
- DIAGNOSTIC HARDENING – Added a saga_diag entry on the classic desktop Swish modal path capturing whether the SDK exposed a usable URL, the qrData shape (data:image vs raw URL) and the top-level result keys, so future merchant-specific SDK shapes can be diagnosed without code changes.
4.0.380
- SWISH MOBILE HOTFIX – Restores the iOS Safari user-gesture window for the swish:// deep link on classic checkout and WooCommerce Blocks. In 4.0.379 the QR modal was mounted BEFORE the redirectToSagaAppUrl(swish://…) call on mobile, and the modal’s DOM activity invalidated the user-gesture allowance iOS Safari requires for custom-scheme navigation. The Swish app then silently failed to open, polling kicked in, and JS eventually navigated to the verify_url which made PHP show “Betalingen ble ikke bekreftet. Kontakt support hvis du ble belastet” almost instantly after Place Order. The mobile branch now redirects FIRST and only mounts the QR modal as a true fallback (when the redirect dispatch fails or no usable swish URL exists).
- SWISH URL NORMALISATION – Classic checkout (checkout.js) now mirrors the express-checkout normaliser: a bare Surfboard payment token returned by the SDK is converted into a real swish://paymentrequest?token=… URL before validation, matching what blocks.js and express-checkout.js already did. This ensures the Open Swish app button and the mobile auto-redirect work on the merchants whose SDK responses only contain the token.
- SWISH QR-AS-DEEPLINK FALLBACK – In all three surfaces (classic, blocks, express) the QR data string is now also tried as a deep-link source for the Open Swish app button and the mobile redirect. Swish QR payloads commonly ARE the swish:// URL itself, so when the SDK only populates the QR field (and not getSwishAppRedirectUrl/nswishAppRedirectUrl) the same string now drives the button — fixing the case where the express quickbuy Swish QR popup showed no Open Swish app button.
4.0.379
- SWISH CHECKOUT UI RESTORED – The Swish QR modal in both classic checkout and WooCommerce Blocks now shows the real bundled Swish logo (assets/images/swish.svg) instead of a green text fallback, matching the design of the Swish express-checkout button. Added a dedicated “Open the Swish app” button to ALL Swish QR surfaces — classic checkout modal, blocks modal AND every express-checkout quickbuy popup (product, cart, checkout) — using the swish:// deep link from the Saga SDK, so customers on mobile (and any desktop visitor with the Swish app installed) can manually launch the Swish app if the automatic redirect is blocked by the browser. The button uses the Swish brand colour (#50BF6F) with the same look across all three surfaces. On classic mobile checkout the swish:// URL is now also passed through to the desktop QR modal path, so a single fallback path covers every device. Title text is unified across all three surfaces (i18n key scan_qr “Skann QR-koden med Swish-appen”). Customer-visible Norwegian/Swedish strings are loaded via i18n with clean UTF-8 fallbacks (was previously double-encoded mojibake such as “Ã…pner Swish-appen…” / “Kunne ikke Ã¥pne Swish-appen. Prøv igjen.”).
4.0.378
- ADMIN VOID NO LONGER LOOPS – The 5-minute cancelled-order safety scan and the central paid-status alias rescue (try_complete_order_from_paid_api_status) now skip orders that an admin has explicitly voided (meta _saga_authorization_voided=yes). Previously a Klarna authorisation that the admin voided via the Void Authorization button would be reactivated five minutes later because Saga’s /orders/{id}/status endpoint still reports orderStatus=PAYMENT_COMPLETED for voided Klarna authorisations, putting the cancelled order back to on-hold and looping the merchant through repeat void clicks. Card voids worked because Saga reflects them on the order status immediately; Klarna does not. With this fix the explicit admin void is authoritative and no scan will undo it.
4.0.377
- DELAYED CAPTURE EVENT MAPPING FIX – Identified the correct terminal event Saga emits in Authorize-only mode. Saga sends order.paymentprocessed (paymentStatus = PAYMENT_PROCESSED) as the customer-facing success event in delayCapture mode — no PAYMENT_COMPLETED webhook arrives until the merchant captures later. The webhook handler, central API success check (is_api_payment_success_status), polling and the PAYMENT_PROCESSED safety net now all treat PAYMENT_PROCESSED (and AUTHORIZED) as a successful terminal state ONLY when delayCapture is enabled. The complete_payment_with_emails() short-circuit still parks such orders on-hold with _saga_payment_status=AUTHORIZED instead of marking them paid, so this never marks an unauthorised order paid in normal mode (where PAYMENT_PROCESSED remains intentionally ambiguous for Vipps/MobilePay). Also extends the cancel/fail webhook API safety check to honour these statuses, so a stray CANCELLED webhook will not cancel a successfully authorised order in delayCapture mode.
4.0.376
- DELAYED CAPTURE REDIRECT FIX – With Capture Mode = “Authorize only (capture later)” the customer is now correctly redirected to the order-received (thank-you) page after a successful authorisation. The central API success check (is_api_payment_success_status) now accepts AUTHORIZED as a verified terminal state when delayCapture is enabled, so finalize AJAX, the webhook handler, polling, cron self-heal and the wallet completion path all treat an authorised payment as a successful customer flow. The complete_payment_with_emails() short-circuit still parks such orders on-hold with _saga_payment_status=AUTHORIZED instead of marking them paid, and on-hold AUTHORIZED orders are now considered ready for the customer redirect. Previously the spinner could remain on the checkout indefinitely waiting for PAYMENT_COMPLETED that never arrives in authorise-only mode, and (worse) the 2-hour pending auto-resolve could eventually cancel the held order.
- I18N FIX – Repaired mojibake in the inline “Click here to try again” recovery button (was rendering literal “Ã¥/ø” instead of “å/ø”) so the Norwegian fallback string now matches the i18n lookup key and shows correctly.
4.0.375
- DELAYED CAPTURE END-TO-END – When “Capture Mode” is set to “Authorize only (capture later)” the plugin now sends controlFunctions.delayCapture=true on every order create and update, holds successfully authorised orders in on-hold with status AUTHORIZED instead of completing them, and uses the documented Saga capture endpoint POST /payments/{paymentId}/capture (replaces the previous /captures call that was not the canonical endpoint). Auto-capture continues to fire when a held order is moved to Processing or Completed, and Auto-Void continues to fire on Cancelled. Subscription / MIT renewals are unaffected and still capture immediately.
- VOID ENDPOINT FIXED – The void/cancel call now uses the documented Saga endpoint PUT …