I eventually copied the configuration of the downloadable product, and applied it to the virtual product:
etc/adminhtml/system.xml:
<section id="catalog">
<group id="virtual_products" translate="label" type="text" sortOrder="650" showInDefault="1"
showInWebsite="1" showInStore="1">
<label>Virtual Product Options</label>
<field id="disable_guest_checkout" translate="label comment" type="select" sortOrder="100"
showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1">
<label>Disable Guest Checkout if Cart Contains Virtual Items</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
</group>
</section>
etc/frontend/events.xml:
<event name="checkout_allow_guest">
<observer name="virtual_checkout_allow_guest"
instance="Vendor\Module\Observer\Virtual\IsAllowedGuestCheckoutObserver" />
</event>
Observer/Virtual/IsAllowedGuestCheckoutObserver.php:
<?php
declare(strict_types=1);
namespace Vendor\Module\Observer\Virtual;
use Magento\Framework\Event\ObserverInterface;
use Magento\Store\Model\ScopeInterface;
/**
* Class IsAllowedGuestCheckoutObserver
* @package Vendor\Module\Observer\Virtual
*/
class IsAllowedGuestCheckoutObserver implements ObserverInterface
{
/**
* Xml path to disable checkout
*/
const XML_PATH_DISABLE_GUEST_CHECKOUT = 'catalog/virtual_products/disable_guest_checkout';
/**
* Core store config
* @var \Magento\Framework\App\Config\ScopeConfigInterface
*/
private $scopeConfig;
/**
* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
*/
public function __construct(
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
) {
$this->scopeConfig = $scopeConfig;
}
/**
* Check is allowed guest checkout if quote contain virtual product(s)
* @param \Magento\Framework\Event\Observer $observer
* @return $this
*/
public function execute(\Magento\Framework\Event\Observer $observer)
{
if (!$this->scopeConfig->isSetFlag(
self::XML_PATH_DISABLE_GUEST_CHECKOUT,
ScopeInterface::SCOPE_STORE,
$observer->getEvent()->getStore()
)) {
return $this;
}
/* @var $quote \Magento\Quote\Model\Quote */
$quote = $observer->getEvent()->getQuote();
foreach ($quote->getAllItems() as $item) {
if (($product = $item->getProduct())
&& $product->getTypeId() == \Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL) {
$observer->getEvent()->getResult()->setIsAllowed(false);
break;
}
}
return $this;
}
}