Compare commits

...
3 Commits
Author SHA1 Message Date
nobswebdev 7222d0c9be show multi-crypto rates in shop nav 2026-09-05 20:50:49 +02:00
nobswebdev 302c3729c4 add multi-crypto payment buttons to cart 2026-09-05 18:55:58 +02:00
nobswebdev 3b6323658e remove crypto rate display from cart 2026-09-05 14:48:01 +02:00
12 changed files with 100 additions and 73 deletions
+6
View File
@@ -0,0 +1,6 @@
import { PaymentMethod } from '../modules/payment/types/PaymentMethod';
export const paymentMethodLabel: Record<PaymentMethod, string> = {
[PaymentMethod.Xmr]: 'XMR',
[PaymentMethod.Btc]: 'BTC'
};
@@ -3,14 +3,13 @@ import { DiscountCodesModule } from '../discountCode/DiscountCodesModule';
import { ProductsModule } from '../product/ProductsModule'; import { ProductsModule } from '../product/ProductsModule';
import { StorefrontCoreModule } from '../storefrontCore/StorefrontCoreModule'; import { StorefrontCoreModule } from '../storefrontCore/StorefrontCoreModule';
import { StorefrontProductModule } from '../storefrontProduct/StorefrontProductModule'; import { StorefrontProductModule } from '../storefrontProduct/StorefrontProductModule';
import { ExchangeRateModule } from '../exchangeRate/ExchangeRateModule';
import { StorefrontCartController } from './controllers/StorefrontCartController'; import { StorefrontCartController } from './controllers/StorefrontCartController';
import { StorefrontCartDiscountResolver } from './services/StorefrontCartDiscountResolver'; import { StorefrontCartDiscountResolver } from './services/StorefrontCartDiscountResolver';
import { StorefrontCartService } from './services/StorefrontCartService'; import { StorefrontCartService } from './services/StorefrontCartService';
import { StorefrontDiscountService } from './services/StorefrontDiscountService'; import { StorefrontDiscountService } from './services/StorefrontDiscountService';
@Module({ @Module({
imports: [StorefrontCoreModule, StorefrontProductModule, ProductsModule, DiscountCodesModule, ExchangeRateModule], imports: [StorefrontCoreModule, StorefrontProductModule, ProductsModule, DiscountCodesModule],
controllers: [StorefrontCartController], controllers: [StorefrontCartController],
providers: [StorefrontCartService, StorefrontDiscountService, StorefrontCartDiscountResolver], providers: [StorefrontCartService, StorefrontDiscountService, StorefrontCartDiscountResolver],
exports: [StorefrontCartService] exports: [StorefrontCartService]
@@ -1,7 +1,10 @@
import { Body, Controller, Get, HttpStatus, Post, Req, Res, UseFilters } from '@nestjs/common'; import { Body, Controller, Get, HttpStatus, Post, Req, Res, UseFilters } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Throttle } from '@nestjs/throttler'; 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 { 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';
import { StorefrontCheckoutSessionCookieService } from '../../storefrontCore/services/StorefrontCheckoutSessionCookieService'; import { StorefrontCheckoutSessionCookieService } from '../../storefrontCore/services/StorefrontCheckoutSessionCookieService';
@@ -33,7 +36,8 @@ export class StorefrontCartController {
private readonly feedbackCookieService: StorefrontFeedbackCookieService, private readonly feedbackCookieService: StorefrontFeedbackCookieService,
private readonly checkoutSessionCookieService: StorefrontCheckoutSessionCookieService, private readonly checkoutSessionCookieService: StorefrontCheckoutSessionCookieService,
private readonly captchaService: StorefrontCaptchaService, private readonly captchaService: StorefrontCaptchaService,
private readonly captchaCookieService: StorefrontCaptchaCookieService private readonly captchaCookieService: StorefrontCaptchaCookieService,
private readonly configService: ConfigService
) {} ) {}
@Get('shop/cart') @Get('shop/cart')
@@ -62,9 +66,17 @@ export class StorefrontCartController {
this.captchaCookieService.setAnswer(req, res, encryptedAnswer); this.captchaCookieService.setAnswer(req, res, encryptedAnswer);
const { enabledPaymentMethods } = this.configService.get('shopSettings') as Config['shopSettings'];
const paymentMethods = enabledPaymentMethods.map(paymentMethod => ({
paymentMethod,
paymentMethodLabel: paymentMethodLabel[paymentMethod]
}));
return res.render('cart-summary', { return res.render('cart-summary', {
...summary, ...summary,
...shopLocals, ...shopLocals,
paymentMethods,
captchaSvg captchaSvg
}); });
} }
@@ -1,8 +1,6 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { DeliveryMode } from '../../product/types/DeliveryMode'; import { DeliveryMode } from '../../product/types/DeliveryMode';
import type { StorefrontProductsService } from '../../storefrontProduct/services/StorefrontProductsService'; import type { StorefrontProductsService } from '../../storefrontProduct/services/StorefrontProductsService';
import type { ExchangeRateService } from '../../exchangeRate/services/ExchangeRateService';
import { PaymentMethod } from '../../payment/types/PaymentMethod';
import type { CookieCartLineExtended } from '../types/CookieCartLineExtended'; import type { CookieCartLineExtended } from '../types/CookieCartLineExtended';
import type { StorefrontDiscountService } from './StorefrontDiscountService'; import type { StorefrontDiscountService } from './StorefrontDiscountService';
import { StorefrontCartService } from './StorefrontCartService'; import { StorefrontCartService } from './StorefrontCartService';
@@ -30,9 +28,6 @@ describe('StorefrontCartService', () => {
getStorefrontVariantsByIds: jest.Mock; getStorefrontVariantsByIds: jest.Mock;
getStorefrontVariant: jest.Mock; getStorefrontVariant: jest.Mock;
}; };
let exchangeRateService: {
getLiveFiatPerCrypto: jest.Mock;
};
let discountService: { let discountService: {
getDiscountStateForCart: jest.Mock; getDiscountStateForCart: jest.Mock;
applyDiscountCode: jest.Mock; applyDiscountCode: jest.Mock;
@@ -44,10 +39,6 @@ describe('StorefrontCartService', () => {
getStorefrontVariant: jest.fn().mockResolvedValue(buildVariant()) getStorefrontVariant: jest.fn().mockResolvedValue(buildVariant())
}; };
exchangeRateService = {
getLiveFiatPerCrypto: jest.fn().mockReturnValue(150)
};
discountService = { discountService = {
getDiscountStateForCart: jest.fn().mockResolvedValue({ getDiscountStateForCart: jest.fn().mockResolvedValue({
discounts: [], discounts: [],
@@ -59,7 +50,6 @@ describe('StorefrontCartService', () => {
service = new StorefrontCartService( service = new StorefrontCartService(
productsService as unknown as StorefrontProductsService, productsService as unknown as StorefrontProductsService,
exchangeRateService as unknown as ExchangeRateService,
discountService as unknown as StorefrontDiscountService discountService as unknown as StorefrontDiscountService
); );
}); });
@@ -99,8 +89,6 @@ describe('StorefrontCartService', () => {
discounts: [], discounts: [],
cartDiscountTotal: 0, cartDiscountTotal: 0,
cartTotalPrice: 0, cartTotalPrice: 0,
cartTotalXmr: null,
fiatPerXmr: null,
hasManualLines: false, hasManualLines: false,
hasAutoLines: false, hasAutoLines: false,
cartTotalIssueMessage: null, cartTotalIssueMessage: null,
@@ -108,23 +96,13 @@ describe('StorefrontCartService', () => {
}); });
}); });
it('converts the discounted total to XMR when a live rate is available', async () => { it('returns cart totals and delivery flags', async () => {
const summary = await service.getCartSummary([{ variantId, qty: 1 }], []); const summary = await service.getCartSummary([{ variantId, qty: 1 }], []);
expect(summary.cartSubtotal).toBe(10); expect(summary.cartSubtotal).toBe(10);
expect(summary.cartTotalPrice).toBe(10); expect(summary.cartTotalPrice).toBe(10);
expect(summary.fiatPerXmr).toBe(150);
expect(summary.cartTotalXmr).toBe('0.06666667');
expect(summary.hasAutoLines).toBe(true); expect(summary.hasAutoLines).toBe(true);
}); expect(summary.hasIssues).toBe(false);
it('leaves cartTotalXmr null when no live rate is available', async () => {
exchangeRateService.getLiveFiatPerCrypto.mockReturnValue(null);
const summary = await service.getCartSummary([{ variantId, qty: 1 }], []);
expect(summary.cartTotalXmr).toBeNull();
expect(summary.fiatPerXmr).toBeNull();
}); });
it('flags manual and auto delivery lines separately', async () => { it('flags manual and auto delivery lines separately', async () => {
@@ -7,9 +7,6 @@ import type { CookieCart } from '../../storefrontCore/types/cart/CookieCart';
import { getQtyByVariantIdFromCart } from '../../../utils/cart/getQtyByVariantIdFromCart'; import { getQtyByVariantIdFromCart } from '../../../utils/cart/getQtyByVariantIdFromCart';
import { DeliveryMode } from '../../product/types/DeliveryMode'; import { DeliveryMode } from '../../product/types/DeliveryMode';
import { StorefrontProductsService } from '../../storefrontProduct/services/StorefrontProductsService'; import { StorefrontProductsService } from '../../storefrontProduct/services/StorefrontProductsService';
import { ExchangeRateService } from '../../exchangeRate/services/ExchangeRateService';
import { PaymentMethod } from '../../payment/types/PaymentMethod';
import { convertFiatToXmr } from '../../../utils/monero/convertFiatToXmr';
import { CookieCartLineDto } from '../dto/CookieCartLineDto'; import { CookieCartLineDto } from '../dto/CookieCartLineDto';
import type { CookieCartExtended } from '../types/CookieCartExtended'; import type { CookieCartExtended } from '../types/CookieCartExtended';
import type { CookieCartLineExtended } from '../types/CookieCartLineExtended'; import type { CookieCartLineExtended } from '../types/CookieCartLineExtended';
@@ -23,7 +20,6 @@ import { StorefrontDiscountService } from './StorefrontDiscountService';
export class StorefrontCartService { export class StorefrontCartService {
constructor( constructor(
private readonly productsService: StorefrontProductsService, private readonly productsService: StorefrontProductsService,
private readonly exchangeRateService: ExchangeRateService,
private readonly discountService: StorefrontDiscountService private readonly discountService: StorefrontDiscountService
) {} ) {}
@@ -67,8 +63,6 @@ export class StorefrontCartService {
discounts: [], discounts: [],
cartDiscountTotal: 0, cartDiscountTotal: 0,
cartTotalPrice: 0, cartTotalPrice: 0,
cartTotalXmr: null,
fiatPerXmr: null,
hasManualLines: false, hasManualLines: false,
hasAutoLines: false, hasAutoLines: false,
cartTotalIssueMessage: null, cartTotalIssueMessage: null,
@@ -78,8 +72,6 @@ export class StorefrontCartService {
const cartSubtotal = sumByKey(cartExtended, 'lineSubtotal'); const cartSubtotal = sumByKey(cartExtended, 'lineSubtotal');
const discountState = await this.discountService.getDiscountStateForCart(discountCodes, cartExtended); const discountState = await this.discountService.getDiscountStateForCart(discountCodes, cartExtended);
const fiatPerXmr = this.exchangeRateService.getLiveFiatPerCrypto(PaymentMethod.Xmr);
const cartTotalXmr = fiatPerXmr !== null ? convertFiatToXmr(discountState.cartTotalPrice, fiatPerXmr) : null;
const hasManualLines = cartExtended.some(line => line.deliveryMode === DeliveryMode.Manual); const hasManualLines = cartExtended.some(line => line.deliveryMode === DeliveryMode.Manual);
const hasAutoLines = cartExtended.some(line => line.deliveryMode === DeliveryMode.Auto); const hasAutoLines = cartExtended.some(line => line.deliveryMode === DeliveryMode.Auto);
const cartTotalIssueMessage = getZeroCartTotalIssue(discountState.cartTotalPrice, cartExtended.length > 0); const cartTotalIssueMessage = getZeroCartTotalIssue(discountState.cartTotalPrice, cartExtended.length > 0);
@@ -92,8 +84,6 @@ export class StorefrontCartService {
cartExtended, cartExtended,
cartSubtotal, cartSubtotal,
...discountState, ...discountState,
cartTotalXmr,
fiatPerXmr,
hasManualLines, hasManualLines,
hasAutoLines, hasAutoLines,
cartTotalIssueMessage, cartTotalIssueMessage,
@@ -4,8 +4,6 @@ import type { CookieCartExtended } from './CookieCartExtended';
export type CookieCartSummary = { export type CookieCartSummary = {
cartExtended: CookieCartExtended; cartExtended: CookieCartExtended;
cartSubtotal: number; cartSubtotal: number;
cartTotalXmr: string | null;
fiatPerXmr: number | null;
hasManualLines: boolean; hasManualLines: boolean;
hasAutoLines: boolean; hasAutoLines: boolean;
cartTotalIssueMessage: string | null; cartTotalIssueMessage: string | null;
@@ -8,6 +8,7 @@ import { StorefrontFeedbackCookieService } from './StorefrontFeedbackCookieServi
import { StorefrontOrderAuthCookieService } from './StorefrontOrderAuthCookieService'; import { StorefrontOrderAuthCookieService } from './StorefrontOrderAuthCookieService';
import { StorefrontThemeCookieService } from './StorefrontThemeCookieService'; import { StorefrontThemeCookieService } from './StorefrontThemeCookieService';
import type { StorefrontPageMetaInput } from '../types/StorefrontPageMetaInput'; import type { StorefrontPageMetaInput } from '../types/StorefrontPageMetaInput';
import { PaymentMethod } from '../../payment/types/PaymentMethod';
describe('StorefrontShopViewService', () => { describe('StorefrontShopViewService', () => {
type ServiceOverrides = { type ServiceOverrides = {
@@ -21,8 +22,9 @@ describe('StorefrontShopViewService', () => {
simplexLink: string | null; simplexLink: string | null;
shippingNote: string | null; shippingNote: string | null;
}; };
shopSettings?: { shopName: string; shopFiatCurrency: string }; shopSettings?: { shopName: string; shopFiatCurrency: string; enabledPaymentMethods?: PaymentMethod[] };
fiatPerXmr?: number; fiatPerXmr?: number;
fiatPerBtc?: number | null;
req?: Partial<Pick<Request, 'protocol' | 'path' | 'originalUrl'>> & { req?: Partial<Pick<Request, 'protocol' | 'path' | 'originalUrl'>> & {
host?: string; host?: string;
}; };
@@ -45,13 +47,20 @@ describe('StorefrontShopViewService', () => {
simplexLink: null, simplexLink: null,
shippingNote: null shippingNote: null
}, },
shopSettings = { shopName: 'Demo Shop', shopFiatCurrency: 'USD' }, shopSettings = {
shopName: 'Demo Shop',
shopFiatCurrency: 'USD',
enabledPaymentMethods: [PaymentMethod.Xmr, PaymentMethod.Btc]
},
fiatPerXmr = 150, fiatPerXmr = 150,
fiatPerBtc = 60_000,
req: reqOverrides = {} req: reqOverrides = {}
} = overrides; } = overrides;
const exchangeRateService = { const exchangeRateService = {
getLiveFiatPerCrypto: jest.fn().mockReturnValue(fiatPerXmr) getLiveFiatPerCrypto: jest.fn((method: PaymentMethod) =>
method === PaymentMethod.Btc ? fiatPerBtc : fiatPerXmr
)
} as unknown as ExchangeRateService; } as unknown as ExchangeRateService;
const configService = { const configService = {
get: jest.fn().mockReturnValue(shopSettings) get: jest.fn().mockReturnValue(shopSettings)
@@ -124,7 +133,8 @@ describe('StorefrontShopViewService', () => {
simplexLink: 'https://simplex.example', simplexLink: 'https://simplex.example',
shippingNote: 'Ships in 3 days' shippingNote: 'Ships in 3 days'
}, },
fiatPerXmr: 200 fiatPerXmr: 200,
fiatPerBtc: 80_000
}); });
const locals = await service.buildShopRenderLocals(req, res, defaultPage); const locals = await service.buildShopRenderLocals(req, res, defaultPage);
@@ -134,7 +144,10 @@ describe('StorefrontShopViewService', () => {
cartTotalQty: 3, cartTotalQty: 3,
feedback: { type: 'success', text: 'Added to cart' }, feedback: { type: 'success', text: 'Added to cart' },
shopFiatCurrency: 'USD', shopFiatCurrency: 'USD',
fiatPerXmr: 200, cryptoRates: [
{ cryptoCurrency: 'XMR', fiatPerCrypto: 200 },
{ cryptoCurrency: 'BTC', fiatPerCrypto: 80_000 }
],
logoUrl: '/uploads/logo.png', logoUrl: '/uploads/logo.png',
faviconUrl: '/uploads/favicon.ico', faviconUrl: '/uploads/favicon.ico',
simplexLink: 'https://simplex.example', simplexLink: 'https://simplex.example',
@@ -264,7 +277,7 @@ describe('StorefrontShopViewService', () => {
it('uses shop fiat currency in product json-ld', async () => { it('uses shop fiat currency in product json-ld', async () => {
const { service, req, res } = createService({ const { service, req, res } = createService({
shopSettings: { shopName: 'Euro Shop', shopFiatCurrency: 'EUR' }, shopSettings: { shopName: 'Euro Shop', shopFiatCurrency: 'EUR', enabledPaymentMethods: [PaymentMethod.Xmr] },
req: { req: {
path: '/shop/products/1/variants/2', path: '/shop/products/1/variants/2',
originalUrl: '/shop/products/1/variants/2' originalUrl: '/shop/products/1/variants/2'
@@ -8,9 +8,11 @@ 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 { 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';
import type { ShopRenderLocals } from '../types/ShopRenderLocals'; import type { ShopRenderLocals } from '../types/ShopRenderLocals';
import type { StorefrontCryptoRate } from '../types/StorefrontCryptoRate';
import type { StorefrontPageMeta } from '../types/StorefrontPageMeta'; import type { StorefrontPageMeta } from '../types/StorefrontPageMeta';
import type { StorefrontPageMetaInput } from '../types/StorefrontPageMetaInput'; import type { StorefrontPageMetaInput } from '../types/StorefrontPageMetaInput';
import type { StorefrontProductJsonLdInput } from '../types/StorefrontProductJsonLdInput'; import type { StorefrontProductJsonLdInput } from '../types/StorefrontProductJsonLdInput';
@@ -39,9 +41,11 @@ export class StorefrontShopViewService {
const cartTotalQty = getTotalCartQtyFromCart(cart); const cartTotalQty = getTotalCartQtyFromCart(cart);
const { shopName, shopFiatCurrency } = this.configService.get('shopSettings') as Config['shopSettings']; const { shopName, shopFiatCurrency, enabledPaymentMethods } = this.configService.get(
'shopSettings'
) as Config['shopSettings'];
const fiatPerXmr = this.exchangeRateService.getLiveFiatPerCrypto(PaymentMethod.Xmr); const cryptoRates = this.buildCryptoRates(enabledPaymentMethods);
const { logoUrl, faviconUrl, simplexLink, shippingNote } = const { logoUrl, faviconUrl, simplexLink, shippingNote } =
await this.shopSettingsService.getStorefrontBranding(); await this.shopSettingsService.getStorefrontBranding();
@@ -61,7 +65,7 @@ export class StorefrontShopViewService {
authorizedOrders, authorizedOrders,
shopNavActive, shopNavActive,
shopFiatCurrency, shopFiatCurrency,
fiatPerXmr, cryptoRates,
logoUrl, logoUrl,
faviconUrl, faviconUrl,
simplexLink, simplexLink,
@@ -130,4 +134,21 @@ export class StorefrontShopViewService {
return JSON.stringify(data); return JSON.stringify(data);
} }
private buildCryptoRates(enabledPaymentMethods: PaymentMethod[]): StorefrontCryptoRate[] {
return enabledPaymentMethods.flatMap(paymentMethod => {
const fiatPerCrypto = this.exchangeRateService.getLiveFiatPerCrypto(paymentMethod);
if (fiatPerCrypto === null) {
return [];
}
return [
{
cryptoCurrency: paymentMethodLabel[paymentMethod],
fiatPerCrypto
}
];
});
}
} }
@@ -1,6 +1,7 @@
import type { ShopFiatCurrency } from '../../../types/ShopFiatCurrency'; import type { ShopFiatCurrency } from '../../../types/ShopFiatCurrency';
import type { AuthorizedOrderNavItem } from './AuthorizedOrderNavItem'; import type { AuthorizedOrderNavItem } from './AuthorizedOrderNavItem';
import type { ShopNavActive } from './ShopNavActive'; import type { ShopNavActive } from './ShopNavActive';
import type { StorefrontCryptoRate } from './StorefrontCryptoRate';
import type { StorefrontFeedback } from './StorefrontFeedback'; import type { StorefrontFeedback } from './StorefrontFeedback';
import type { StorefrontPageMeta } from './StorefrontPageMeta'; import type { StorefrontPageMeta } from './StorefrontPageMeta';
import type { StorefrontThemePreference } from './StorefrontThemePreference'; import type { StorefrontThemePreference } from './StorefrontThemePreference';
@@ -13,7 +14,7 @@ export type ShopRenderLocals = {
authorizedOrders: AuthorizedOrderNavItem[]; authorizedOrders: AuthorizedOrderNavItem[];
shopNavActive: ShopNavActive; shopNavActive: ShopNavActive;
shopFiatCurrency: ShopFiatCurrency; shopFiatCurrency: ShopFiatCurrency;
fiatPerXmr: number | null; cryptoRates: StorefrontCryptoRate[];
logoUrl: string | null; logoUrl: string | null;
faviconUrl: string | null; faviconUrl: string | null;
simplexLink: string | null; simplexLink: string | null;
@@ -0,0 +1,4 @@
export type StorefrontCryptoRate = {
cryptoCurrency: string;
fiatPerCrypto: number;
};
@@ -52,13 +52,6 @@
{{cartTotalPrice}} {{cartTotalPrice}}
{{shopFiatCurrency}} {{shopFiatCurrency}}
</p> </p>
{{#if cartTotalXmr}}
<p class='sf-text-muted'>= {{cartTotalXmr}} XMR</p>
{{else}}
<p class='sf-text-muted'>
We couldn't determine the XMR rate right now. Please try again later.
</p>
{{/if}}
<form class='sf-form-row' method='post' action='/shop/cart/discount'> <form class='sf-form-row' method='post' action='/shop/cart/discount'>
<label class='sf-field'> <label class='sf-field'>
@@ -73,22 +66,30 @@
{{/if}} {{/if}}
<div class='sf-stack sf-mt-4'> <div class='sf-stack sf-mt-4'>
{{#if cartTotalXmr}} {{#unless hasIssues}}
{{#unless hasIssues}} <form class='sf-form-col' method='post' action='/shop/checkout/pay'>
<form class='sf-form-col' method='post' action='/shop/checkout/pay'> <div>{{{captchaSvg}}}</div>
<div>{{{captchaSvg}}}</div> <label class='sf-field'>
<label class='sf-field'> <span class='sf-field__label'>Enter the code above</span>
<span class='sf-field__label'>Enter the code above</span> <input class='sf-input' type='text' name='captcha' autocomplete='off' required />
<input class='sf-input' type='text' name='captcha' autocomplete='off' required /> </label>
</label>
<button type='submit' class='sf-btn sf-btn--primary'>Pay with XMR</button> <div class='sf-stack'>
</form> {{#each paymentMethods}}
{{else}} <button
<button type='button' class='sf-btn sf-btn--primary' disabled title='Resolve cart issues before paying'>Pay with XMR</button> type='submit'
{{/unless}} name='paymentMethod'
value='{{paymentMethod}}'
class='sf-btn sf-btn--primary'
>
Pay with {{paymentMethodLabel}}
</button>
{{/each}}
</div>
</form>
{{else}} {{else}}
<button type='button' class='sf-btn sf-btn--primary' disabled title='XMR rate unavailable'>Pay with XMR</button> <button type='button' class='sf-btn sf-btn--primary' disabled title='Resolve cart issues before paying'>Continue to payment</button>
{{/if}} {{/unless}}
{{> manual-shipping-info}} {{> manual-shipping-info}}
{{> auto-delivery-info}} {{> auto-delivery-info}}
@@ -29,8 +29,12 @@
</nav> </nav>
</div> </div>
{{#if fiatPerXmr}} {{#if cryptoRates.length}}
<div class='sf-rate'>1 XMR = {{fiatPerXmr}} {{shopFiatCurrency}}</div> <div class='sf-rates'>
{{#each cryptoRates}}
<div class='sf-rate'>1 {{cryptoCurrency}} = {{fiatPerCrypto}} {{../shopFiatCurrency}}</div>
{{/each}}
</div>
{{/if}} {{/if}}
</div> </div>
</header> </header>