Compare commits

...
2 Commits
Author SHA1 Message Date
nobswebdev 68503ea01d Add payment method icons to storefront navbar and cart.
Show BTC/XMR icons beside exchange rates and pay buttons.
2026-09-07 12:28:14 +02:00
nobswebdev dd234421a4 Rename cryptoCurrency to paymentLabel in storefront invoice views.
Align invoice view naming with InvoiceExtended and CMS.
2026-09-07 12:28:09 +02:00
17 changed files with 71 additions and 22 deletions
@@ -0,0 +1,6 @@
import { PaymentMethod } from '../modules/payment/types/PaymentMethod';
export const paymentMethodIconUrl: Record<PaymentMethod, string> = {
[PaymentMethod.Xmr]: '/shop/assets/img/xmr.png',
[PaymentMethod.Btc]: '/shop/assets/img/btc.png'
};
@@ -4,6 +4,7 @@ import { Throttle } from '@nestjs/throttler';
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { throttleProfiles } from '../../../config/throttleProfiles'; import { throttleProfiles } from '../../../config/throttleProfiles';
import type { Config } from '../../../types/Config'; import type { Config } from '../../../types/Config';
import { paymentMethodIconUrl } from '../../../consts/paymentMethodIconUrl';
import { paymentMethodLabel } from '../../../consts/paymentMethodLabel'; import { paymentMethodLabel } from '../../../consts/paymentMethodLabel';
import { StorefrontExceptionFilter } from '../../storefrontCore/filters/StorefrontExceptionFilter'; import { StorefrontExceptionFilter } from '../../storefrontCore/filters/StorefrontExceptionFilter';
import { StorefrontCartCookieService } from '../../storefrontCore/services/StorefrontCartCookieService'; import { StorefrontCartCookieService } from '../../storefrontCore/services/StorefrontCartCookieService';
@@ -70,7 +71,8 @@ export class StorefrontCartController {
const paymentMethods = enabledPaymentMethods.map(paymentMethod => ({ const paymentMethods = enabledPaymentMethods.map(paymentMethod => ({
paymentMethod, paymentMethod,
paymentMethodLabel: paymentMethodLabel[paymentMethod] paymentMethodLabel: paymentMethodLabel[paymentMethod],
iconUrl: paymentMethodIconUrl[paymentMethod]
})); }));
return res.render('cart-summary', { return res.render('cart-summary', {
@@ -56,7 +56,7 @@ describe('StorefrontCheckoutViewService', () => {
let toStorefrontInvoiceViewSpy: jest.SpiedFunction<typeof toStorefrontInvoiceViewModule.toStorefrontInvoiceView>; let toStorefrontInvoiceViewSpy: jest.SpiedFunction<typeof toStorefrontInvoiceViewModule.toStorefrontInvoiceView>;
const checkoutInvoiceView = { const checkoutInvoiceView = {
cryptoCurrency: 'XMR', paymentLabel: 'XMR',
amountFiat: 9 amountFiat: 9
} as unknown as StorefrontInvoiceView; } as unknown as StorefrontInvoiceView;
@@ -355,9 +355,31 @@ a:hover {
gap: var(--sf-space-1); gap: var(--sf-space-1);
} }
.sf-rates {
display: flex;
flex-wrap: wrap;
gap: var(--sf-space-3);
align-items: center;
}
.sf-rate { .sf-rate {
display: inline-flex;
align-items: center;
gap: var(--sf-space-1);
font-size: 0.95rem; font-size: 0.95rem;
color: var(--sf-text-muted); color: var(--sf-text-muted);
white-space: nowrap;
}
.sf-crypto-icon {
width: 1.25rem;
height: 1.25rem;
object-fit: contain;
flex-shrink: 0;
}
.sf-btn--pay {
gap: var(--sf-space-2);
} }
.sf-category-nav { .sf-category-nav {
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@@ -145,8 +145,8 @@ describe('StorefrontShopViewService', () => {
feedback: { type: 'success', text: 'Added to cart' }, feedback: { type: 'success', text: 'Added to cart' },
shopFiatCurrency: 'USD', shopFiatCurrency: 'USD',
cryptoRates: [ cryptoRates: [
{ cryptoCurrency: 'XMR', fiatPerCrypto: 200 }, { paymentLabel: 'XMR', fiatPerCrypto: 200, iconUrl: '/shop/assets/img/xmr.png' },
{ cryptoCurrency: 'BTC', fiatPerCrypto: 80_000 } { paymentLabel: 'BTC', fiatPerCrypto: 80_000, iconUrl: '/shop/assets/img/btc.png' }
], ],
logoUrl: '/uploads/logo.png', logoUrl: '/uploads/logo.png',
faviconUrl: '/uploads/favicon.ico', faviconUrl: '/uploads/favicon.ico',
@@ -8,6 +8,7 @@ import { formatShortOrderId } from '../../../utils/order/formatShortOrderId';
import { toAbsoluteUrl } from '../../../utils/toAbsoluteUrl'; import { toAbsoluteUrl } from '../../../utils/toAbsoluteUrl';
import { ShopSettingsService } from '../../shopSettings/services/ShopSettingsService'; import { ShopSettingsService } from '../../shopSettings/services/ShopSettingsService';
import { ExchangeRateService } from '../../exchangeRate/services/ExchangeRateService'; import { ExchangeRateService } from '../../exchangeRate/services/ExchangeRateService';
import { paymentMethodIconUrl } from '../../../consts/paymentMethodIconUrl';
import { paymentMethodLabel } from '../../../consts/paymentMethodLabel'; import { paymentMethodLabel } from '../../../consts/paymentMethodLabel';
import { PaymentMethod } from '../../payment/types/PaymentMethod'; import { PaymentMethod } from '../../payment/types/PaymentMethod';
import type { AuthorizedOrderNavItem } from '../types/AuthorizedOrderNavItem'; import type { AuthorizedOrderNavItem } from '../types/AuthorizedOrderNavItem';
@@ -145,8 +146,9 @@ export class StorefrontShopViewService {
return [ return [
{ {
cryptoCurrency: paymentMethodLabel[paymentMethod], paymentLabel: paymentMethodLabel[paymentMethod],
fiatPerCrypto fiatPerCrypto,
iconUrl: paymentMethodIconUrl[paymentMethod]
} }
]; ];
}); });
@@ -1,4 +1,5 @@
export type StorefrontCryptoRate = { export type StorefrontCryptoRate = {
cryptoCurrency: string; paymentLabel: string;
fiatPerCrypto: number; fiatPerCrypto: number;
iconUrl: string;
}; };
@@ -3,7 +3,7 @@ import type { InvoiceStatusLabel } from '../../../utils/invoice/types/InvoiceSta
import type { InvoiceStatusVariant } from '../../../utils/invoice/types/InvoiceStatusVariant'; import type { InvoiceStatusVariant } from '../../../utils/invoice/types/InvoiceStatusVariant';
export type StorefrontInvoiceView = { export type StorefrontInvoiceView = {
cryptoCurrency: string; paymentLabel: string;
expectedTotalCrypto: string; expectedTotalCrypto: string;
receivedTotalCrypto: string | null; receivedTotalCrypto: string | null;
paymentAddress: string; paymentAddress: string;
@@ -23,14 +23,14 @@
<p class='sf-payment-status sf-payment-status--confirmed'>Payment received</p> <p class='sf-payment-status sf-payment-status--confirmed'>Payment received</p>
<p class='sf-payment-detail'> <p class='sf-payment-detail'>
Received: Received:
<strong>{{checkout.checkoutInvoice.receivedTotalCrypto}} {{checkout.checkoutInvoice.cryptoCurrency}}</strong> <strong>{{checkout.checkoutInvoice.receivedTotalCrypto}} {{checkout.checkoutInvoice.paymentLabel}}</strong>
</p> </p>
<p class='sf-payment-detail'> <p class='sf-payment-detail'>
Your order is being prepared. This page will redirect automatically once it is ready. Your order is being prepared. This page will redirect automatically once it is ready.
</p> </p>
{{> refresh-link href=checkout.refreshHref showAutoRefreshNote=true}} {{> refresh-link href=checkout.refreshHref showAutoRefreshNote=true}}
{{else}} {{else}}
<h2 class='sf-section-title'>Pay with {{checkout.checkoutInvoice.cryptoCurrency}}</h2> <h2 class='sf-section-title'>Pay with {{checkout.checkoutInvoice.paymentLabel}}</h2>
{{#with checkout.checkoutInvoice}} {{#with checkout.checkoutInvoice}}
{{> invoice-payment refreshHref=../checkout.refreshHref showAutoRefreshNote=true}} {{> invoice-payment refreshHref=../checkout.refreshHref showAutoRefreshNote=true}}
{{/with}} {{/with}}
@@ -80,9 +80,16 @@
type='submit' type='submit'
name='paymentMethod' name='paymentMethod'
value='{{paymentMethod}}' value='{{paymentMethod}}'
class='sf-btn sf-btn--primary' class='sf-btn sf-btn--primary sf-btn--pay'
> >
Pay with {{paymentMethodLabel}} Pay with {{paymentMethodLabel}}
{{> storefront-image
class='sf-crypto-icon'
src=iconUrl
alt=''
width='20'
height='20'
}}
</button> </button>
{{/each}} {{/each}}
</div> </div>
@@ -3,27 +3,27 @@
{{/if}} {{/if}}
{{#if showProminentAmount}} {{#if showProminentAmount}}
<p class='sf-payment-amount'>{{expectedTotalCrypto}} {{cryptoCurrency}}</p> <p class='sf-payment-amount'>{{expectedTotalCrypto}} {{paymentLabel}}</p>
{{/if}} {{/if}}
{{#if showReceivedTotal}} {{#if showReceivedTotal}}
<p class='sf-payment-detail'> <p class='sf-payment-detail'>
Received: Received:
<strong>{{receivedTotalCrypto}} {{cryptoCurrency}}</strong> <strong>{{receivedTotalCrypto}} {{paymentLabel}}</strong>
</p> </p>
{{/if}} {{/if}}
{{#if showExpectedTotal}} {{#if showExpectedTotal}}
<p class='sf-payment-detail'> <p class='sf-payment-detail'>
Expected total: Expected total:
<strong>{{expectedTotalCrypto}} {{cryptoCurrency}}</strong> <strong>{{expectedTotalCrypto}} {{paymentLabel}}</strong>
</p> </p>
{{/if}} {{/if}}
{{#if showInstruction}} {{#if showInstruction}}
<p class='sf-payment-detail'> <p class='sf-payment-detail'>
{{instructionPrefix}} {{instructionPrefix}}
<strong>{{instructionAmountCrypto}} {{cryptoCurrency}}</strong> <strong>{{instructionAmountCrypto}} {{paymentLabel}}</strong>
{{instructionSuffix}} {{instructionSuffix}}
{{#if showExpiry}} {{#if showExpiry}}
Payment expires in Payment expires in
@@ -34,7 +34,7 @@
{{#if showQr}} {{#if showQr}}
<div class='sf-qr-wrap'> <div class='sf-qr-wrap'>
<img src='{{qrCodeUrl}}' width='220' height='220' alt='{{cryptoCurrency}} payment QR code' /> <img src='{{qrCodeUrl}}' width='220' height='220' alt='{{paymentLabel}} payment QR code' />
</div> </div>
{{/if}} {{/if}}
@@ -48,7 +48,7 @@
<ul class='sf-list-reset sf-tx-list'> <ul class='sf-list-reset sf-tx-list'>
{{#each payments}} {{#each payments}}
<li class='sf-tx-item'> <li class='sf-tx-item'>
<div><strong>{{amountCrypto}} {{../cryptoCurrency}}</strong></div> <div><strong>{{amountCrypto}} {{../paymentLabel}}</strong></div>
<div class='sf-mono sf-text-subtle'>{{txHash}}</div> <div class='sf-mono sf-text-subtle'>{{txHash}}</div>
<div class='sf-tx-status sf-tx-status--{{confirmationStatusVariant}}'>{{confirmationStatus}}</div> <div class='sf-tx-status sf-tx-status--{{confirmationStatusVariant}}'>{{confirmationStatus}}</div>
</li> </li>
@@ -32,7 +32,16 @@
{{#if cryptoRates.length}} {{#if cryptoRates.length}}
<div class='sf-rates'> <div class='sf-rates'>
{{#each cryptoRates}} {{#each cryptoRates}}
<div class='sf-rate'>1 {{cryptoCurrency}} = {{fiatPerCrypto}} {{../shopFiatCurrency}}</div> <span class='sf-rate'>
{{> storefront-image
class='sf-crypto-icon'
src=iconUrl
alt=paymentLabel
width='20'
height='20'
}}
<span>{{fiatPerCrypto}} {{../shopFiatCurrency}}</span>
</span>
{{/each}} {{/each}}
</div> </div>
{{/if}} {{/if}}
@@ -198,7 +198,7 @@ describe('StorefrontOrderViewService', () => {
expect(view.totals.shippingCostFiat).toBe(5); expect(view.totals.shippingCostFiat).toBe(5);
expect(view.totals.grandTotalFiat).toBe(15); expect(view.totals.grandTotalFiat).toBe(15);
expect(view.shippingInvoice).toMatchObject({ expect(view.shippingInvoice).toMatchObject({
cryptoCurrency: 'XMR', paymentLabel: 'XMR',
paymentAddress: '4shipping' paymentAddress: '4shipping'
}); });
}); });
@@ -109,7 +109,7 @@ describe('toStorefrontInvoiceView', () => {
const view = await toStorefrontInvoiceView(invoice); const view = await toStorefrontInvoiceView(invoice);
expect(view).toMatchObject({ expect(view).toMatchObject({
cryptoCurrency: 'XMR', paymentLabel: 'XMR',
expectedTotalCrypto: '1.00000000', expectedTotalCrypto: '1.00000000',
receivedTotalCrypto: null, receivedTotalCrypto: null,
paymentAddress, paymentAddress,
@@ -154,7 +154,7 @@ describe('toStorefrontInvoiceView', () => {
const view = await toStorefrontInvoiceView(invoice); const view = await toStorefrontInvoiceView(invoice);
expect(view).toMatchObject({ expect(view).toMatchObject({
cryptoCurrency: 'BTC', paymentLabel: 'BTC',
expectedTotalCrypto: '1.00000000', expectedTotalCrypto: '1.00000000',
paymentAddress: 'bc1qstorefronttest' paymentAddress: 'bc1qstorefronttest'
}); });
@@ -114,7 +114,7 @@ const buildStorefrontInvoiceView = async ({
const statusVariant = resolveInvoiceStatusVariant(invoiceState); const statusVariant = resolveInvoiceStatusVariant(invoiceState);
return { return {
cryptoCurrency: paymentLabel, paymentLabel,
expectedTotalCrypto, expectedTotalCrypto,
receivedTotalCrypto, receivedTotalCrypto,
paymentAddress: invoice.paymentAddress, paymentAddress: invoice.paymentAddress,