diff --git a/Api/Data/Quote/ExtendedShippingInformationInterface.php b/Api/Data/Quote/ExtendedShippingInformationInterface.php deleted file mode 100644 index 6c2c28f..0000000 --- a/Api/Data/Quote/ExtendedShippingInformationInterface.php +++ /dev/null @@ -1,23 +0,0 @@ -config = $config; + } + + protected function _toHtml() + { + if ($this->config->getIsEnableCtaCartPage() && !$this->config->getIsEnableCartPageHeadless()) { + return parent::_toHtml(); + } + + return ''; + } +} diff --git a/Block/Cta/CartHeadless.php b/Block/Cta/CartHeadless.php new file mode 100644 index 0000000..209f9ea --- /dev/null +++ b/Block/Cta/CartHeadless.php @@ -0,0 +1,35 @@ +config = $config; + } + + protected function _toHtml() + { + /** @var \Afterpay\Afterpay\ViewModel\Container\Cta\Headless $viewModel */ + $viewModel = $this->getViewModel(); + if ($viewModel && $viewModel->isContainerEnable() && $this->isEnabledForCart()) { + return parent::_toHtml(); + } + + return ''; + } + public function isEnabledForCart(): bool + { + return $this->config->getIsEnableCtaCartPage() && $this->config->getIsEnableCartPageHeadless(); + } +} diff --git a/Block/Cta/MiniCartHeadless.php b/Block/Cta/MiniCartHeadless.php new file mode 100644 index 0000000..8af5700 --- /dev/null +++ b/Block/Cta/MiniCartHeadless.php @@ -0,0 +1,36 @@ +config = $config; + } + + protected function _toHtml() + { + /** @var \Afterpay\Afterpay\ViewModel\Container\Cta\Headless $viewModel */ + $viewModel = $this->getViewModel(); + if ($viewModel && $viewModel->isContainerEnable() && $this->isEnabledForMinicart()) { + return parent::_toHtml(); + } + + return ''; + } + + public function isEnabledForMinicart(): bool + { + return $this->config->getIsEnableCtaMiniCart() && $this->config->getIsEnableMiniCartHeadless(); + } +} diff --git a/Block/Cta/Product.php b/Block/Cta/Product.php new file mode 100644 index 0000000..275ffc8 --- /dev/null +++ b/Block/Cta/Product.php @@ -0,0 +1,29 @@ +config = $config; + } + + protected function _toHtml() + { + if ($this->config->getIsEnableCtaProductPage() && !$this->config->getIsEnableProductPageHeadless()) { + return parent::_toHtml(); + } + + return ''; + } +} diff --git a/Block/Cta/ProductHeadless.php b/Block/Cta/ProductHeadless.php new file mode 100644 index 0000000..9f67610 --- /dev/null +++ b/Block/Cta/ProductHeadless.php @@ -0,0 +1,32 @@ +config = $config; + } + + protected function _toHtml() + { + /** @var \Afterpay\Afterpay\ViewModel\Container\Cta\Headless $viewModel */ + $viewModel = $this->getViewModel(); + if ($viewModel && $viewModel->isContainerEnable() + && $this->config->getIsEnableCtaProductPage() && $this->config->getIsEnableProductPageHeadless()) { + return parent::_toHtml(); + } + + return ''; + } +} diff --git a/Block/ExpressCheckout/Cart.php b/Block/ExpressCheckout/Cart.php new file mode 100644 index 0000000..1692d40 --- /dev/null +++ b/Block/ExpressCheckout/Cart.php @@ -0,0 +1,29 @@ +config = $config; + } + + protected function _toHtml() + { + if ($this->config->getIsEnableExpressCheckoutCartPage() && !$this->config->getIsEnableCartPageHeadless()) { + return parent::_toHtml(); + } + + return ''; + } +} diff --git a/Block/ExpressCheckout/CartHeadless.php b/Block/ExpressCheckout/CartHeadless.php new file mode 100644 index 0000000..0d8156a --- /dev/null +++ b/Block/ExpressCheckout/CartHeadless.php @@ -0,0 +1,35 @@ +config = $config; + } + + protected function _toHtml() + { + /** @var \Afterpay\Afterpay\ViewModel\Container\ExpressCheckout\Headless $viewModel */ + $viewModel = $this->getViewModel(); + if ($viewModel && $viewModel->isContainerEnable() && $this->isEnabledForCart()) { + return parent::_toHtml(); + } + + return ''; + } + public function isEnabledForCart(): bool + { + return $this->config->getIsEnableExpressCheckoutCartPage() && $this->config->getIsEnableCartPageHeadless(); + } +} diff --git a/Block/ExpressCheckout/MiniCartHeadless.php b/Block/ExpressCheckout/MiniCartHeadless.php new file mode 100644 index 0000000..c1ef88b --- /dev/null +++ b/Block/ExpressCheckout/MiniCartHeadless.php @@ -0,0 +1,36 @@ +config = $config; + } + + protected function _toHtml() + { + /** @var \Afterpay\Afterpay\ViewModel\Container\ExpressCheckout\Headless $viewModel */ + $viewModel = $this->getViewModel(); + if ($viewModel && $viewModel->isContainerEnable() && $this->isEnabledForMinicart()) { + return parent::_toHtml(); + } + + return ''; + } + + public function isEnabledForMinicart(): bool + { + return $this->config->getIsEnableExpressCheckoutMiniCart() && $this->config->getIsEnableMiniCartHeadless(); + } +} diff --git a/Block/ExpressCheckout/Product.php b/Block/ExpressCheckout/Product.php new file mode 100644 index 0000000..c73c5fd --- /dev/null +++ b/Block/ExpressCheckout/Product.php @@ -0,0 +1,29 @@ +config = $config; + } + + protected function _toHtml() + { + if ($this->config->getIsEnableExpressCheckoutProductPage() && !$this->config->getIsEnableProductPageHeadless()) { + return parent::_toHtml(); + } + + return ''; + } +} diff --git a/Block/ExpressCheckout/ProductHeadless.php b/Block/ExpressCheckout/ProductHeadless.php new file mode 100644 index 0000000..3baeea8 --- /dev/null +++ b/Block/ExpressCheckout/ProductHeadless.php @@ -0,0 +1,32 @@ +config = $config; + } + + protected function _toHtml() + { + /** @var \Afterpay\Afterpay\ViewModel\Container\ExpressCheckout\Headless $viewModel */ + $viewModel = $this->getViewModel(); + if ($viewModel && $viewModel->isContainerEnable() + && $this->config->getIsEnableExpressCheckoutProductPage() && $this->config->getIsEnableProductPageHeadless()) { + return parent::_toHtml(); + } + + return ''; + } +} diff --git a/Gateway/Request/ExpressCheckoutDataBuilder.php b/Gateway/Request/ExpressCheckoutDataBuilder.php index 12db725..5702565 100644 --- a/Gateway/Request/ExpressCheckoutDataBuilder.php +++ b/Gateway/Request/ExpressCheckoutDataBuilder.php @@ -4,19 +4,6 @@ class ExpressCheckoutDataBuilder extends \Afterpay\Afterpay\Gateway\Request\Checkout\CheckoutDataBuilder { - private \Afterpay\Afterpay\Api\Data\Quote\ExtendedShippingInformationInterface $extendedShippingInformation; - - public function __construct( - \Magento\Framework\UrlInterface $url, - \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, - \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, - \Afterpay\Afterpay\Model\CBT\CheckCBTCurrencyAvailabilityInterface $checkCBTCurrencyAvailability, - \Afterpay\Afterpay\Api\Data\Quote\ExtendedShippingInformationInterface $extendedShippingInformation - ) { - parent::__construct($url, $productRepository, $searchCriteriaBuilder, $checkCBTCurrencyAvailability); - $this->extendedShippingInformation = $extendedShippingInformation; - } - public function build(array $buildSubject): array { /** @var \Magento\Quote\Model\Quote $quote */ @@ -26,10 +13,11 @@ public function build(array $buildSubject): array $amount = $isCBTCurrencyAvailable ? $quote->getGrandTotal() : $quote->getBaseGrandTotal(); $currencyCode = $isCBTCurrencyAvailable ? $currentCurrencyCode : $quote->getBaseCurrencyCode(); $popupOriginUrl = $buildSubject['popup_origin_url']; - $lastSelectedShippingRate = $this->extendedShippingInformation->getParam( - $quote, - \Afterpay\Afterpay\Api\Data\Quote\ExtendedShippingInformationInterface::LAST_SELECTED_SHIPPING_RATE - ); + + $lastSelectedShippingRate = null; + if ($quote->getShippingAddress() && $quote->getShippingAddress()->getShippingMethod()) { + $lastSelectedShippingRate = $quote->getShippingAddress()->getShippingMethod(); + } $data = [ 'mode' => 'express', diff --git a/Gateway/Response/Checkout/CheckoutDataToQuoteHandler.php b/Gateway/Response/Checkout/CheckoutDataToQuoteHandler.php index 5c9e55a..c3cacbb 100644 --- a/Gateway/Response/Checkout/CheckoutDataToQuoteHandler.php +++ b/Gateway/Response/Checkout/CheckoutDataToQuoteHandler.php @@ -68,7 +68,9 @@ public function handle(array $handlingSubject, array $response): void } $shippingInformation->setBillingAddress($address); - if (!$quote->isVirtual()) { + if ($quote->isVirtual()) { + $shippingInformation->setShippingAddress($address); // to avoid an error with gift cart registry + } else { $explodedShippingOption = explode('_', $response['shippingOptionIdentifier']); $carrierCode = array_shift($explodedShippingOption); $methodCode = implode('_', $explodedShippingOption); diff --git a/Gateway/Response/MerchantConfiguration/CreditMemoOnGrandTotalConfigurationHandler.php b/Gateway/Response/MerchantConfiguration/CreditMemoOnGrandTotalConfigurationHandler.php new file mode 100644 index 0000000..b6c617d --- /dev/null +++ b/Gateway/Response/MerchantConfiguration/CreditMemoOnGrandTotalConfigurationHandler.php @@ -0,0 +1,25 @@ +config = $config; + } + + public function handle(array $handlingSubject, array $response): void + { + $websiteId = (int)$handlingSubject['websiteId']; + $mpid = $response['publicId'] ?? ''; + $flagValue = false; // TODO: replace it with a flag pull + $this->config->setIsCreditMemoGrandTotalOnlyEnabled((int)$flagValue, $websiteId); + } +} diff --git a/Model/Config.php b/Model/Config.php index 652606c..f0b32c6 100644 --- a/Model/Config.php +++ b/Model/Config.php @@ -39,6 +39,20 @@ class Config public const XML_PATH_CASHAPP_PAY_ACTIVE = 'payment/cashapp/active'; public const XML_PATH_CONSUMER_LENDING_ENABLED = 'payment/afterpay/consumer_lending_enabled'; public const XML_PATH_CONSUMER_LENDING_MIN_AMOUNT = 'payment/afterpay/consumer_lending_min_amount'; + public const XML_PATH_ENABLE_CREDIT_MEMO_GRANDTOTAL_ONLY = 'payment/afterpay/enable_creditmemo_grandtotal_only'; + public const XML_PATH_ENABLE_PRODUCT_HEADLESS = 'payment/afterpay/enable_product_page_headless'; + public const XML_PATH_PDP_PLACEMENT_AFTER_SELECTOR = 'payment/afterpay/pdp_placement_after_selector'; + public const XML_PATH_PDP_PLACEMENT_PRICE_SELECTOR = 'payment/afterpay/pdp_placement_price_selector'; + public const XML_PATH_PDP_PLACEMENT_AFTER_SELECTOR_BUNDLE = 'payment/afterpay/pdp_placement_after_selector_bundle'; + public const XML_PATH_PDP_PLACEMENT_PRICE_SELECTOR_BUNDLE = 'payment/afterpay/pdp_placement_price_selector_bundle'; + public const XML_PATH_ENABLE_MINI_CART_HEADLESS = 'payment/afterpay/enable_mini_cart_headless'; + public const XML_PATH_MINI_CART_PLACEMENT_CONTAINER_SELECTOR = 'payment/afterpay/mini_cart_placement_container_selector'; + public const XML_PATH_MINI_CART_PLACEMENT_AFTER_SELECTOR = 'payment/afterpay/mini_cart_placement_after_selector'; + public const XML_PATH_MINI_CART_PLACEMENT_PRICE_SELECTOR = 'payment/afterpay/mini_cart_placement_price_selector'; + public const XML_PATH_ENABLE_CART_PAGE_HEADLESS = 'payment/afterpay/enable_cart_page_headless'; + public const XML_PATH_CART_PAGE_PLACEMENT_AFTER_SELECTOR = 'payment/afterpay/cart_page_placement_after_selector'; + public const XML_PATH_CART_PAGE_PLACEMENT_PRICE_SELECTOR = 'payment/afterpay/cart_page_placement_price_selector'; + private ScopeConfigInterface $scopeConfig; private WriterInterface $writer; @@ -46,10 +60,10 @@ class Config private SerializerInterface $serializer; public function __construct( - ScopeConfigInterface $scopeConfig, - WriterInterface $writer, - ResourceConnection $resourceConnection, - SerializerInterface $serializer + ScopeConfigInterface $scopeConfig, + WriterInterface $writer, + ResourceConnection $resourceConnection, + SerializerInterface $serializer ) { $this->scopeConfig = $scopeConfig; $this->writer = $writer; @@ -569,6 +583,40 @@ public function getAddLastSelectedShipRate(?int $scopeCode = null): bool ); } + public function getIsCreditMemoGrandTotalOnlyEnabled(?int $websiteId = null, bool $fromApi = false): bool + { + if ($fromApi) { + $flagValue = false; // TODO: replace it with a flag pull + $this->setIsCreditMemoGrandTotalOnlyEnabled((int)$flagValue, $websiteId); + + return $flagValue; + } + + return $this->scopeConfig->isSetFlag( + self::XML_PATH_ENABLE_CREDIT_MEMO_GRANDTOTAL_ONLY, + ScopeInterface::SCOPE_WEBSITE, + $websiteId + ); + } + + public function setIsCreditMemoGrandTotalOnlyEnabled(int $value, int $scopeId = 0): self + { + if ($scopeId) { + $this->writer->save( + self::XML_PATH_ENABLE_CREDIT_MEMO_GRANDTOTAL_ONLY, + $value, + ScopeInterface::SCOPE_WEBSITES, + $scopeId + ); + return $this; + } + $this->writer->save( + self::XML_PATH_ENABLE_CREDIT_MEMO_GRANDTOTAL_ONLY, + $value + ); + return $this; + } + public function setCashAppPayActive(int $value, int $scopeId = 0): self { if ($scopeId) { @@ -595,4 +643,112 @@ public function getCashAppPayActive(?int $scopeCode = null): bool $scopeCode ); } + + public function getIsEnableProductPageHeadless(?int $scopeId = null): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_ENABLE_PRODUCT_HEADLESS, + ScopeInterface::SCOPE_WEBSITE, + $scopeId + ); + } + + public function getPdpPlacementAfterSelector(?int $scopeId = null): string + { + return (string)$this->scopeConfig->getValue( + self::XML_PATH_PDP_PLACEMENT_AFTER_SELECTOR, + ScopeInterface::SCOPE_WEBSITE, + $scopeId + ); + } + + public function getPdpPlacementPriceSelector(?int $scopeId = null): string + { + return (string)$this->scopeConfig->getValue( + self::XML_PATH_PDP_PLACEMENT_PRICE_SELECTOR, + ScopeInterface::SCOPE_WEBSITE, + $scopeId + ); + } + + public function getPdpPlacementAfterSelectorBundle(?int $scopeId = null): string + { + return (string)$this->scopeConfig->getValue( + self::XML_PATH_PDP_PLACEMENT_AFTER_SELECTOR_BUNDLE, + ScopeInterface::SCOPE_WEBSITE, + $scopeId + ); + } + + public function getPdpPlacementPriceSelectorBundle(?int $scopeId = null): string + { + return (string)$this->scopeConfig->getValue( + self::XML_PATH_PDP_PLACEMENT_PRICE_SELECTOR_BUNDLE, + ScopeInterface::SCOPE_WEBSITE, + $scopeId + ); + } + + public function getIsEnableMiniCartHeadless(?int $scopeId = null): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_ENABLE_MINI_CART_HEADLESS, + ScopeInterface::SCOPE_WEBSITE, + $scopeId + ); + } + + public function getMiniCartPlacementContainerSelector(?int $scopeId = null): string + { + return (string)$this->scopeConfig->getValue( + self::XML_PATH_MINI_CART_PLACEMENT_CONTAINER_SELECTOR, + ScopeInterface::SCOPE_WEBSITE, + $scopeId + ); + } + + public function getMiniCartPlacementAfterSelector(?int $scopeId = null): string + { + return (string)$this->scopeConfig->getValue( + self::XML_PATH_MINI_CART_PLACEMENT_AFTER_SELECTOR, + ScopeInterface::SCOPE_WEBSITE, + $scopeId + ); + } + + public function getMiniCartPlacementPriceSelector(?int $scopeId = null): string + { + return (string)$this->scopeConfig->getValue( + self::XML_PATH_MINI_CART_PLACEMENT_PRICE_SELECTOR, + ScopeInterface::SCOPE_WEBSITE, + $scopeId + ); + } + + public function getIsEnableCartPageHeadless(?int $scopeId = null): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_ENABLE_CART_PAGE_HEADLESS, + ScopeInterface::SCOPE_WEBSITE, + $scopeId + ); + } + + public function getCartPagePlacementAfterSelector(?int $scopeId = null): string + { + return (string)$this->scopeConfig->getValue( + self::XML_PATH_CART_PAGE_PLACEMENT_AFTER_SELECTOR, + ScopeInterface::SCOPE_WEBSITE, + $scopeId + ); + } + + public function getCartPagePlacementPriceSelector(?int $scopeId = null): string + { + return (string)$this->scopeConfig->getValue( + self::XML_PATH_CART_PAGE_PLACEMENT_PRICE_SELECTOR, + ScopeInterface::SCOPE_WEBSITE, + $scopeId + ); + } } diff --git a/Model/GraphQl/Resolver/AfterpayConfig.php b/Model/GraphQl/Resolver/AfterpayConfig.php index 8e8845d..4913e78 100644 --- a/Model/GraphQl/Resolver/AfterpayConfig.php +++ b/Model/GraphQl/Resolver/AfterpayConfig.php @@ -4,14 +4,16 @@ use Afterpay\Afterpay\Model\Config; use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Store\Model\StoreManagerInterface; class AfterpayConfig implements ResolverInterface { - private Config $config; - private StoreManagerInterface $storeManager; + protected Config $config; + protected StoreManagerInterface $storeManager; public function __construct( Config $config, @@ -21,6 +23,18 @@ public function __construct( $this->storeManager = $storeManager; } + /** + * Fetches the data from persistence models and format it according to the GraphQL schema. + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array|Value + * @throws \Exception + */ public function resolve( Field $field, $context, diff --git a/Model/GraphQl/Resolver/AfterpayConfigCart.php b/Model/GraphQl/Resolver/AfterpayConfigCart.php new file mode 100644 index 0000000..5b3e4ce --- /dev/null +++ b/Model/GraphQl/Resolver/AfterpayConfigCart.php @@ -0,0 +1,90 @@ +cartRepository = $cartRepository; + $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + } + + /** + * Fetches the data from persistence models and format it according to the GraphQL schema. + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return mixed|Value + * @throws \Exception + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$args || !$args['input']) { + throw new \InvalidArgumentException('Required params cart_id and redirect_path are missing'); + } + + $storeId = $args['input']['store_id']; + $cartId = $args['input']['cart_id']; + if (!is_numeric($cartId) && !empty($cartId)) { + $cartId = $this->maskedQuoteIdToQuoteId->execute($cartId); + } + $cart = $this->cartRepository->get($cartId); + $store = $this->storeManager->getStore($storeId); + $this->storeManager->setCurrentStore($store); + $websiteId = (int)$store->getWebsiteId(); + + $result = parent::resolve($field, $context, $info, $value, $args); + + $result['website_id'] = $websiteId; + $result['is_enabled_cta_cart_page_headless'] = $this->config->getIsEnableCartPageHeadless($websiteId); + $result['is_enabled_ec_cart_page_headless'] = $this->config->getIsEnableCartPageHeadless($websiteId); + $result['placement_after_selector'] = $this->config->getCartPagePlacementAfterSelector($websiteId); + $result['price_selector'] = $this->config->getCartPagePlacementPriceSelector($websiteId); + $result['show_lover_limit'] = $this->config->getMinOrderTotal($websiteId) >= 1; + $result['is_cbt_enabled'] = count($this->config->getSpecificCountries($websiteId)) > 1; + $result['is_product_allowed'] = true; + $result['is_virtual'] = $cart->isVirtual(); + $excludedCategoriesIds = $this->config->getExcludeCategories((int)$storeId); + if (!empty($excludedCategoriesIds)) { + foreach ($cart->getAllVisibleItems() as $item) { + foreach ($item->getProduct()->getCategoryIds() as $categoryId) { + if (in_array($categoryId, $excludedCategoriesIds)) { + $result['is_product_allowed'] = false; + break; + } + } + } + } + + return $result; + } +} diff --git a/Model/GraphQl/Resolver/AfterpayConfigMiniCart.php b/Model/GraphQl/Resolver/AfterpayConfigMiniCart.php new file mode 100644 index 0000000..0d5ade5 --- /dev/null +++ b/Model/GraphQl/Resolver/AfterpayConfigMiniCart.php @@ -0,0 +1,43 @@ +config->getIsEnableMiniCartHeadless($websiteId); + $result['is_enabled_ec_minicart_headless'] = $this->config->getIsEnableMiniCartHeadless($websiteId); + $result['placement_wrapper'] = $this->config->getMiniCartPlacementContainerSelector($websiteId); + $result['placement_after_selector'] = $this->config->getMiniCartPlacementAfterSelector($websiteId); + $result['price_selector'] = $this->config->getMiniCartPlacementPriceSelector($websiteId); + + return $result; + } +} diff --git a/Model/GraphQl/Resolver/AfterpayConfigPdp.php b/Model/GraphQl/Resolver/AfterpayConfigPdp.php new file mode 100644 index 0000000..38554f2 --- /dev/null +++ b/Model/GraphQl/Resolver/AfterpayConfigPdp.php @@ -0,0 +1,81 @@ +productRepository = $productRepository; + } + + /** + * Fetches the data from persistence models and format it according to the GraphQL schema. + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return mixed|Value + * @throws \Exception + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$args || !$args['input']) { + throw new \InvalidArgumentException('Required params cart_id and redirect_path are missing'); + } + + $storeId = $args['input']['store_id']; + $productSku = $args['input']['product_sku']; + $product = $this->productRepository->get($productSku); + $store = $this->storeManager->getStore($storeId); + $this->storeManager->setCurrentStore($store); + $websiteId = (int)$store->getWebsiteId(); + + $result = parent::resolve($field, $context, $info, $value, $args); + + $result['product_type'] = $product->getTypeId(); + $result['is_enabled_cta_pdp_headless'] = $this->config->getIsEnableProductPageHeadless($websiteId); + $result['is_enabled_ec_pdp_headless'] = $this->config->getIsEnableProductPageHeadless($websiteId); + $result['placement_after_selector'] = $this->config->getPdpPlacementAfterSelector($websiteId); + $result['price_selector'] = $this->config->getPdpPlacementPriceSelector($websiteId); + $result['placement_after_selector_bundle'] = $this->config->getPdpPlacementAfterSelectorBundle($websiteId); + $result['price_selector_bundle'] = $this->config->getPdpPlacementPriceSelectorBundle($websiteId); + $result['show_lover_limit'] = $this->config->getMinOrderTotal($websiteId) >= 1; + $result['is_cbt_enabled'] = count($this->config->getSpecificCountries($websiteId)) > 1; + $result['is_product_allowed'] = true; + $excludedCategoriesIds = $this->config->getExcludeCategories((int)$storeId); + if (!empty($excludedCategoriesIds)) { + foreach ($product->getCategoryIds() as $categoryId) { + if (in_array($categoryId, $excludedCategoriesIds)) { + $result['is_product_allowed'] = false; + break; + } + } + } + + return $result; + } +} diff --git a/Model/Order/CreditMemo/CreditMemoInitiator.php b/Model/Order/CreditMemo/CreditMemoInitiator.php index b57d7f8..fa75a74 100644 --- a/Model/Order/CreditMemo/CreditMemoInitiator.php +++ b/Model/Order/CreditMemo/CreditMemoInitiator.php @@ -15,8 +15,23 @@ public function __construct( public function init(\Magento\Sales\Model\Order $order): \Magento\Sales\Model\Order\Creditmemo { $qtysToRefund = []; + /** @var \Magento\Sales\Model\Order\Item $orderItem */ foreach ($order->getItemsCollection() as $orderItem) { - /** @var $orderItem \Magento\Sales\Model\Order\Item */ + if ($orderItem->getProductType() == \Magento\Bundle\Model\Product\Type::TYPE_CODE) { + /** @var \Magento\Sales\Model\Order\Item $childrenItem */ + foreach ($orderItem->getChildrenItems() as $childrenItem) { + if (!$childrenItem->getIsVirtual()) { + $qtyShipped = $childrenItem->getQtyShipped(); + $qtyOrdered = $childrenItem->getQtyOrdered(); + $qtyRefunded = $childrenItem->getQtyRefunded(); + $childItemLeftToShip = $qtyOrdered - ($qtyShipped + $qtyRefunded); + if ($childItemLeftToShip > 0) { + $qtysToRefund[$childrenItem->getId()] = $childItemLeftToShip; + } + } + } + } + if (!$orderItem->getParentItem() && !$orderItem->getIsVirtual()) { $qtyShipped = $orderItem->getQtyShipped(); $qtyOrdered = $orderItem->getQtyOrdered(); diff --git a/Model/Payment/AmountProcessor/CreditMemo.php b/Model/Payment/AmountProcessor/CreditMemo.php index fd5061c..0cf22c4 100644 --- a/Model/Payment/AmountProcessor/CreditMemo.php +++ b/Model/Payment/AmountProcessor/CreditMemo.php @@ -20,6 +20,20 @@ public function process(\Magento\Sales\Model\Order\Payment $payment): array { $amountToRefund = $amountToVoid = 0; + if ($this->config->getIsCreditMemoGrandTotalOnlyEnabled((int)$payment->getOrder()->getStore()->getWebsiteId(), true)) { + $this->processWithGrandTotal($payment, $amountToVoid, $amountToRefund); + } else { + $this->processWithSeparateCalculations($payment, $amountToVoid, $amountToRefund); + } + + return [$amountToRefund, $amountToVoid]; + } + + private function processWithSeparateCalculations( + \Magento\Sales\Model\Order\Payment $payment, + float &$amountToVoid, + float &$amountToRefund + ): void { $creditmemo = $payment->getCreditmemo(); foreach ($creditmemo->getAllItems() as $creditmemoItem) { $orderItem = $creditmemoItem->getOrderItem(); @@ -49,12 +63,10 @@ public function process(\Magento\Sales\Model\Order\Payment $payment): array } $this->processShipmentAmount($payment, $creditmemo, $amountToRefund, $amountToVoid); - $this->processCapturedDiscountForRefundAmount($payment, $amountToRefund); $this->processRolloverDiscountForVoidAmount($payment, $amountToVoid); $this->processAdjustmentAmount($payment, $amountToVoid, $amountToRefund); - return [$amountToRefund, $amountToVoid]; } private function processAdjustmentAmount( @@ -218,4 +230,43 @@ private function getItemCapturedQty(\Magento\Sales\Model\Order\Item $item): floa } return 0; } + + private function processWithGrandTotal( + \Magento\Sales\Model\Order\Payment $payment, + float &$amountToVoid, + float &$amountToRefund + ): void { + $isCBTCurrency = $payment->getAdditionalInformation(\Afterpay\Afterpay\Api\Data\CheckoutInterface::AFTERPAY_IS_CBT_CURRENCY); + $paymentState = $payment->getAdditionalInformation(\Afterpay\Afterpay\Model\Payment\AdditionalInformationInterface::AFTERPAY_PAYMENT_STATE); + $creditmemo = $payment->getCreditmemo(); + $amount = $isCBTCurrency ? $creditmemo->getGrandTotal() : $creditmemo->getBaseGrandTotal(); + + switch ($paymentState) { + case \Afterpay\Afterpay\Model\PaymentStateInterface::AUTH_APPROVED: + $amountToVoid += $amount; + break; + case \Afterpay\Afterpay\Model\PaymentStateInterface::PARTIALLY_CAPTURED: + $openToCapture = $payment->getAdditionalInformation( + \Afterpay\Afterpay\Model\Payment\AdditionalInformationInterface::AFTERPAY_OPEN_TO_CAPTURE_AMOUNT + ); + $orderAmount = $isCBTCurrency ? $payment->getOrder()->getGrandTotal() : $payment->getOrder()->getBaseGrandTotal(); + + if ($amount == $orderAmount) { + if ($openToCapture && $amount > $openToCapture) { + $amountToVoid += $openToCapture; + $amountToRefund += $amount - $openToCapture; + } else { + $amountToRefund += $amount; + } + } else { + $this->processWithSeparateCalculations($payment, $amountToVoid, $amountToRefund); + } + break; + case \Afterpay\Afterpay\Model\PaymentStateInterface::CAPTURED: + default: + $amountToRefund += $amount; + break; + } + } + } diff --git a/Model/Quote/ExtendedShippingInformation.php b/Model/Quote/ExtendedShippingInformation.php deleted file mode 100644 index 56d6a57..0000000 --- a/Model/Quote/ExtendedShippingInformation.php +++ /dev/null @@ -1,53 +0,0 @@ -cartRepository = $cartRepository; - $this->serializer = $serializer; - } - - public function update(\Magento\Quote\Model\Quote $quote, string $param, $data): \Magento\Quote\Model\Quote - { - $extShippingInfo = $quote->getExtShippingInfo(); - if ($extShippingInfo) { - $extShippingInfo = $this->serializer->unserialize($extShippingInfo); - } - - if (!$extShippingInfo) { - $extShippingInfo = []; - } - - if (is_array($extShippingInfo)) { - $extShippingInfo[$param] = $data; - $quote->setExtShippingInfo($this->serializer->serialize($extShippingInfo)); - - $this->cartRepository->save($quote); - } - - return $quote; - } - - public function getParam(\Magento\Quote\Model\Quote $quote, string $param) - { - $extShippingInfo = $quote->getExtShippingInfo(); - - if ($extShippingInfo) { - $extShippingInfo = $this->serializer->unserialize($extShippingInfo); - - if (isset($extShippingInfo[$param])) { - return $extShippingInfo[$param]; - } - } - - return $extShippingInfo; - } -} diff --git a/Plugin/Checkout/Block/Cart/Sidebar.php b/Plugin/Checkout/Block/Cart/Sidebar.php index 41106fe..72d69c0 100644 --- a/Plugin/Checkout/Block/Cart/Sidebar.php +++ b/Plugin/Checkout/Block/Cart/Sidebar.php @@ -33,15 +33,18 @@ public function afterGetJsLayout(\Magento\Checkout\Block\Cart\Sidebar $sidebar, ) { $result = $this->ctaContainerViewModel->updateJsLayout( $result, - !($this->config->getIsEnableCtaMiniCart() && - $this->ctaContainerViewModel->isContainerEnable()) + !($this->config->getIsEnableCtaMiniCart() + && $this->ctaContainerViewModel->isContainerEnable() + && !$this->config->getIsEnableMiniCartHeadless()) ); $result = $this->expressCheckoutViewModel->updateJsLayout( $result, !($this->config->getIsEnableExpressCheckoutMiniCart() && - $this->expressCheckoutViewModel->isContainerEnable()) + $this->expressCheckoutViewModel->isContainerEnable() + && !$this->config->getIsEnableMiniCartHeadless()) ); } + return $result; } } diff --git a/Plugin/Checkout/Model/TotalsInformationManagement/AddLastSelectedShippingRate.php b/Plugin/Checkout/Model/TotalsInformationManagement/AddLastSelectedShippingRate.php deleted file mode 100644 index 44a9bea..0000000 --- a/Plugin/Checkout/Model/TotalsInformationManagement/AddLastSelectedShippingRate.php +++ /dev/null @@ -1,64 +0,0 @@ -cartRepository = $cartRepository; - $this->extendedShippingInformation = $extendedShippingInformation; - $this->config = $config; - } - - public function afterCalculate( - TotalsInformationManagementInterface $subject, - TotalsInterface $result, - $cartId, - TotalsInformationInterface $addressInformation - ) { - if (!$this->config->getAddLastSelectedShipRate() - || (!$this->config->getIsEnableExpressCheckoutMiniCart() - && !$this->config->getIsEnableExpressCheckoutProductPage() - && !$this->config->getIsEnableExpressCheckoutCartPage())) { - return $result; - } - - /** @var Quote $quote */ - $quote = $this->cartRepository->get($cartId); - $shippingRate = ''; - if ($quote->getShippingAddress()->getShippingMethod()) { - $shippingRate = $quote->getShippingAddress()->getShippingMethod(); - } elseif ($addressInformation->getShippingCarrierCode() && $addressInformation->getShippingMethodCode()) { - $shippingRate = implode( - '_', - [$addressInformation->getShippingCarrierCode(), $addressInformation->getShippingMethodCode()] - ); - } - - if ($shippingRate) { - $this->extendedShippingInformation->update( - $quote, - ExtendedShippingInformationInterface::LAST_SELECTED_SHIPPING_RATE, - $shippingRate - ); - } - - return $result; - } -} diff --git a/ViewModel/Container/Cta/Headless.php b/ViewModel/Container/Cta/Headless.php new file mode 100644 index 0000000..1ddafd7 --- /dev/null +++ b/ViewModel/Container/Cta/Headless.php @@ -0,0 +1,62 @@ +registry = $registry; + $this->localeResolver = $localeResolver; + $this->checkoutSession = $checkoutSession; + } + + public function getProductSku(): string + { + return $this->registry->registry('current_product')->getSku(); + } + + public function getStoreId(): string + { + return (string)$this->storeManager->getStore()->getId(); + } + + public function getCurrency(): string + { + return $this->storeManager->getStore()->getCurrentCurrencyCode(); + } + + public function getLocale(): string + { + return $this->localeResolver->getLocale(); + } + + public function getCartId(): string + { + return (string)$this->checkoutSession->getQuoteId(); + } +} diff --git a/ViewModel/Container/ExpressCheckout/ExpressCheckout.php b/ViewModel/Container/ExpressCheckout/ExpressCheckout.php index f91dd35..97229d0 100644 --- a/ViewModel/Container/ExpressCheckout/ExpressCheckout.php +++ b/ViewModel/Container/ExpressCheckout/ExpressCheckout.php @@ -26,7 +26,7 @@ public function updateJsLayout( return parent::updateJsLayout($jsLayoutJson, $remove, $containerNodeName, $config); } - private function getCountryCode(): ?string + public function getCountryCode(): ?string { $currencyCode = $this->storeManager->getStore()->getCurrentCurrencyCode(); return static::COUNTRY_CURRENCY_MAP[$currencyCode] ?? null; diff --git a/ViewModel/Container/ExpressCheckout/Headless.php b/ViewModel/Container/ExpressCheckout/Headless.php new file mode 100644 index 0000000..0ce3aee --- /dev/null +++ b/ViewModel/Container/ExpressCheckout/Headless.php @@ -0,0 +1,46 @@ +checkoutSession = $checkoutSession; + $this->registry = $registry; + } + + public function getProductSku(): string + { + return $this->registry->registry('current_product')->getSku(); + } + + public function getStoreId(): string + { + return (string)$this->storeManager->getStore()->getId(); + } + + public function getCartId(): string + { + return (string)$this->checkoutSession->getQuoteId(); + } +} diff --git a/composer.json b/composer.json index 2226b8e..784327d 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "license": "Apache-2.0", "type": "magento2-module", "description": "Magento 2 Afterpay Payment Module", - "version": "5.3.2", + "version": "5.4.0", "require": { "php": ">=7.4.0", "magento/framework": "^103.0", diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 18cbb73..ed27aa5 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -6,7 +6,7 @@ Afterpay\Afterpay\Block\Adminhtml\System\Config\Fieldset\Payment - + @@ -95,10 +95,6 @@ Magento\Config\Model\Config\Source\Yesno payment/afterpay/enable_cta_mini_cart - - - payment/afterpay/sort_order - Afterpay\Afterpay\Model\Config\Source\Category @@ -119,25 +115,120 @@ Not recommended if an external Order Management System (OMS) is in use.]]> - + Afterpay\Afterpay\Block\Adminhtml\System\Config\Fieldset\ExpressCheckout - + Magento\Config\Model\Config\Source\Yesno payment/afterpay/enable_express_checkout_product_page - + Magento\Config\Model\Config\Source\Yesno payment/afterpay/enable_express_checkout_cart_page - + Magento\Config\Model\Config\Source\Yesno payment/afterpay/enable_express_checkout_mini_cart + + + + + + + Magento\Config\Model\Config\Source\Yesno + payment/afterpay/enable_product_page_headless + Enable advanced assets settings for the Product Page. + + + + payment/afterpay/pdp_placement_after_selector + .product-info-main .product-info-price
Default for Hyvä or similar themes: .price-box.price-final_price]]>
+ + 1 + +
+ + + payment/afterpay/pdp_placement_price_selector + .product-info-main .price-final_price .price-wrapper .price
Default for Hyvä or similar themes: .final-price .price-wrapper .price]]>
+ + 1 + +
+ + + payment/afterpay/pdp_placement_after_selector_bundle + #bundleSummary .price-configured_price
Default for Hyvä or similar themes: #bundleSummary .product-details]]>
+ + 1 + +
+ + + payment/afterpay/pdp_placement_price_selector_bundle + #bundleSummary .price-as-configured .price
Default for Hyvä or similar themes: #bundleSummary .price-wrapper .price]]>
+ + 1 + +
+ + + Magento\Config\Model\Config\Source\Yesno + payment/afterpay/enable_cart_page_headless + Enable advanced assets settings for the Cart Page. + + + + payment/afterpay/cart_page_placement_after_selector + #cart-totals
Default for Hyvä or similar themes: #cart-totals]]>
+ + 1 + +
+ + + payment/afterpay/cart_page_placement_price_selector + #cart-totals .grand.totals .price
Default for Hyvä or similar themes: #cart-totals > div:last-child [x-text="hyva.formatPrice(segment.value)"]]]>
+ + 1 + +
+ + + Magento\Config\Model\Config\Source\Yesno + payment/afterpay/enable_mini_cart_headless + Enable advanced assets settings for the Mini Cart. + + + + payment/afterpay/mini_cart_placement_after_selector + #minicart-content-wrapper .subtotal
Default for Hyvä or similar themes: #cart-drawer [x-ref="cartDialogContent"] > div > div:last-child > div > div]]>
+ + 1 + +
+ + + payment/afterpay/mini_cart_placement_container_selector + #minicart-content-wrapper
Default for Hyvä or similar themes: #cart-drawer]]>
+ + 1 + +
+ + + payment/afterpay/mini_cart_placement_price_selector + #minicart-content-wrapper .subtotal .price
Default for Hyvä or similar themes: [x-html="cart.subtotal"]]]>
+ + 1 + +
+
diff --git a/etc/config.xml b/etc/config.xml index 646671c..d4efd65 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -24,6 +24,18 @@ AU,NZ,US,CA 1 + 0 + .product-info-main .product-info-price + .product-info-main .price-final_price .price-wrapper .price + #bundleSummary .price-configured_price + #bundleSummary .price-as-configured .price + 0 + #minicart-content-wrapper + #minicart-content-wrapper .subtotal + #minicart-content-wrapper .subtotal .price + 0 + .cart-container .cart-summary #cart-totals + #cart-totals .grand.totals .price diff --git a/etc/di.xml b/etc/di.xml index 20d5999..706e20a 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -7,7 +7,6 @@ - @@ -172,6 +171,7 @@ Afterpay\Afterpay\Gateway\Response\MerchantConfiguration\MpidConfigurationHandler Afterpay\Afterpay\Gateway\Response\MerchantConfiguration\ChannelsConfigurationHandler Afterpay\Afterpay\Gateway\Response\MerchantConfiguration\ConsumerLendingConfigurationHandler + Afterpay\Afterpay\Gateway\Response\MerchantConfiguration\CreditMemoOnGrandTotalConfigurationHandler diff --git a/etc/module.xml b/etc/module.xml index 5dd812c..139abcd 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,7 +1,7 @@ - + diff --git a/etc/schema.graphqls b/etc/schema.graphqls index a169ba2..76c4bb7 100644 --- a/etc/schema.graphqls +++ b/etc/schema.graphqls @@ -30,6 +30,24 @@ type Query { afterpayConfig: afterpayConfigOutput @resolver(class: "Afterpay\\Afterpay\\Model\\GraphQl\\Resolver\\AfterpayConfig") @doc(description:"return Afterpay config") } +type Mutation { + getAfterpayConfigPdp(input: getAfterpayConfigPdpInput): afterpayConfigPdpOutput @resolver(class: "Afterpay\\Afterpay\\Model\\GraphQl\\Resolver\\AfterpayConfigPdp") @doc(description:"return Afterpay PDP config") + getAfterpayConfigCart(input: getAfterpayConfigCartInput): afterpayConfigCartOutput @resolver(class: "Afterpay\\Afterpay\\Model\\GraphQl\\Resolver\\AfterpayConfigCart") @doc(description:"return Afterpay Cart config") + getAfterpayConfigMiniCart(input: getAfterpayConfigCartInput): afterpayConfigMiniCartOutput @resolver(class: "Afterpay\\Afterpay\\Model\\GraphQl\\Resolver\\AfterpayConfigMiniCart") @doc(description:"return Afterpay Mini Cart config") +} + + +input getAfterpayConfigPdpInput { + store_id: String! + product_sku: String! +} + +input getAfterpayConfigCartInput { + store_id: String! + cart_id: String! +} + + type afterpayConfigOutput { max_amount: String min_amount: String @@ -41,3 +59,55 @@ type afterpayConfigOutput { api_mode: String mpid: String } + +type afterpayConfigPdpOutput { + max_amount: String + min_amount: String + allowed_currencies: [String] + is_enabled: Boolean + api_mode: String + mpid: String + is_enabled_cta_pdp_headless: Boolean + is_enabled_ec_pdp_headless: Boolean + product_type: String + show_lover_limit: Boolean + is_product_allowed: Boolean + is_cbt_enabled: Boolean + placement_after_selector: String + placement_after_selector_bundle: String + price_selector: String + price_selector_bundle: String +} + +type afterpayConfigCartOutput { + allowed_currencies: [String] + is_enabled: Boolean + mpid: String + is_enabled_cta_cart_page_headless: Boolean + is_enabled_ec_cart_page_headless: Boolean + show_lover_limit: Boolean + is_product_allowed: Boolean + is_cbt_enabled: Boolean + placement_after_selector: String + price_selector: String + max_amount: String + min_amount: String + is_virtual: Boolean +} + +type afterpayConfigMiniCartOutput { + allowed_currencies: [String] + is_enabled: Boolean + mpid: String + is_enabled_cta_minicart_headless: Boolean + is_enabled_ec_minicart_headless: Boolean + show_lover_limit: Boolean + is_product_allowed: Boolean + is_cbt_enabled: Boolean + placement_wrapper: String + placement_after_selector: String + price_selector: String + max_amount: String + min_amount: String + is_virtual: Boolean +} diff --git a/etc/webapi_rest/di.xml b/etc/webapi_rest/di.xml deleted file mode 100644 index 5aa2b9f..0000000 --- a/etc/webapi_rest/di.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/view/frontend/layout/catalog_product_view.xml b/view/frontend/layout/catalog_product_view.xml index a468089..b762d57 100644 --- a/view/frontend/layout/catalog_product_view.xml +++ b/view/frontend/layout/catalog_product_view.xml @@ -10,6 +10,11 @@ Afterpay\Afterpay\ViewModel\Container\Cta\PDPLib + + + Afterpay\Afterpay\ViewModel\Container\Cta\Headless + + Afterpay\Afterpay\ViewModel\Container\ExpressCheckout\PDPLib @@ -17,7 +22,12 @@ - + + + Afterpay\Afterpay\ViewModel\Container\ExpressCheckout\Headless + + + Afterpay\Afterpay\ViewModel\Container\ExpressCheckout\ExpressCheckoutPdp @@ -43,7 +53,7 @@ - + Afterpay\Afterpay\ViewModel\Container\Cta\Cta diff --git a/view/frontend/layout/checkout_cart_index.xml b/view/frontend/layout/checkout_cart_index.xml index ea1fbc3..a657f78 100644 --- a/view/frontend/layout/checkout_cart_index.xml +++ b/view/frontend/layout/checkout_cart_index.xml @@ -10,14 +10,37 @@ Afterpay\Afterpay\ViewModel\Container\Cta\CartLib + + + Afterpay\Afterpay\ViewModel\Container\Cta\Headless + + Afterpay\Afterpay\ViewModel\Container\ExpressCheckout\CartLib + + + Afterpay\Afterpay\ViewModel\Container\ExpressCheckout\Headless + + + + + Afterpay\Afterpay\ViewModel\Container\ExpressCheckout\Headless + + - + Afterpay\Afterpay\ViewModel\Container\Cta\Cta @@ -43,7 +66,7 @@ - + Afterpay\Afterpay\ViewModel\Container\ExpressCheckout\ExpressCheckout diff --git a/view/frontend/layout/checkout_cart_sidebar_total_renderers.xml b/view/frontend/layout/checkout_cart_sidebar_total_renderers.xml index 15be899..cf60861 100644 --- a/view/frontend/layout/checkout_cart_sidebar_total_renderers.xml +++ b/view/frontend/layout/checkout_cart_sidebar_total_renderers.xml @@ -16,6 +16,22 @@ + + + + Afterpay\Afterpay\ViewModel\Container\Cta\Headless + + + + + Afterpay\Afterpay\ViewModel\Container\ExpressCheckout\Headless + + + diff --git a/view/frontend/layout/checkout_onepage_success.xml b/view/frontend/layout/checkout_onepage_success.xml new file mode 100644 index 0000000..ce71dea --- /dev/null +++ b/view/frontend/layout/checkout_onepage_success.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/view/frontend/templates/cta/cart/headless.phtml b/view/frontend/templates/cta/cart/headless.phtml new file mode 100644 index 0000000..f33cc80 --- /dev/null +++ b/view/frontend/templates/cta/cart/headless.phtml @@ -0,0 +1,21 @@ +getViewModel(); +$jsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/cta/cart/headless.js'); +?> + + diff --git a/view/frontend/templates/cta/minicart/headless.phtml b/view/frontend/templates/cta/minicart/headless.phtml new file mode 100644 index 0000000..ef2c19a --- /dev/null +++ b/view/frontend/templates/cta/minicart/headless.phtml @@ -0,0 +1,15 @@ +getViewModel(); +$jsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/cta/minicart/headless.js'); +?> + + diff --git a/view/frontend/templates/cta/pdp/headless.phtml b/view/frontend/templates/cta/pdp/headless.phtml new file mode 100644 index 0000000..e5085bd --- /dev/null +++ b/view/frontend/templates/cta/pdp/headless.phtml @@ -0,0 +1,13 @@ +getViewModel(); +$jsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/cta/pdp/headless.js'); +?> + + diff --git a/view/frontend/templates/express-checkout/cart-headless.phtml b/view/frontend/templates/express-checkout/cart-headless.phtml new file mode 100644 index 0000000..28ed580 --- /dev/null +++ b/view/frontend/templates/express-checkout/cart-headless.phtml @@ -0,0 +1,34 @@ +getViewModel(); +$jsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/express-checkout/cart/headless.js'); +$externalJsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/express-checkout/cart/express-checkout-cart-widget.js'); +?> + + + + + + + diff --git a/view/frontend/templates/express-checkout/headless.phtml b/view/frontend/templates/express-checkout/headless.phtml new file mode 100644 index 0000000..325b15b --- /dev/null +++ b/view/frontend/templates/express-checkout/headless.phtml @@ -0,0 +1,34 @@ +getViewModel(); +$jsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/express-checkout/product/headless.js'); +$externalJsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/express-checkout/product/express-checkout-widget.js'); +?> + + + + + + + diff --git a/view/frontend/templates/express-checkout/minicart-headless.phtml b/view/frontend/templates/express-checkout/minicart-headless.phtml new file mode 100644 index 0000000..5fb4adb --- /dev/null +++ b/view/frontend/templates/express-checkout/minicart-headless.phtml @@ -0,0 +1,28 @@ +getViewModel(); +$jsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/express-checkout/minicart/headless.js'); +$externalJsUrl = $block->getViewFileUrl('Afterpay_Afterpay::js/view/container/express-checkout/minicart/express-checkout-minicart-widget.js'); +?> + + + + + + + diff --git a/view/frontend/web/css/afterpay-express-checkout.less b/view/frontend/web/css/afterpay-express-checkout.less index 1d308e8..18be2a1 100644 --- a/view/frontend/web/css/afterpay-express-checkout.less +++ b/view/frontend/web/css/afterpay-express-checkout.less @@ -13,9 +13,57 @@ } .afterpay-express-checkout-minicart-wraper { margin: 0 10px 15px; + position: relative; + z-index: 9; .afterpay-express-button, button.afterpay-express-button:hover { width: 100%; cursor: pointer; text-align: center; } } + +.headless-afterpay-ec .afterpay-express-button, +.headless-afterpay-ec button.afterpay-express-button:hover, +.headless-afterpay-ec .afterpay-express-button-minicart, +.headless-afterpay-ec button.afterpay-express-button-minicart:hover { + background-image: none; + background: #000; + border: 1px solid #000; + color: #ffffff; + cursor: pointer; + display: inline-block; + float: none; + width: 267px; + max-width: 100%; + margin-top: 10px; +} + +.headless-afterpay-ec .afterpay-express-button-minicart, +.headless-afterpay-ec button.afterpay-express-button-minicart:hover { + margin: 0 10px; + width: 100%; + max-width: 328px; + padding: 2px 15px; +} + +.headless-afterpay-ec .afterpay-express-button-cart, +.headless-afterpay-ec button.afterpay-express-button-cart:hover { + background: #000; + border: none; +} + +.headless-afterpay-ec.hidden { + display: none; +} + +.checkout-onepage-success .success.message { + display: none; +} + +.checkout-onepage-success .success.message.show-message { + display: block; +} + +.hyva_checkout-index-index #payment-method-option-afterpay { + display: none; +} diff --git a/view/frontend/web/js/service/container/pricebox-widget-mixin.js b/view/frontend/web/js/service/container/pricebox-widget-mixin.js index ad96ac0..0c44573 100644 --- a/view/frontend/web/js/service/container/pricebox-widget-mixin.js +++ b/view/frontend/web/js/service/container/pricebox-widget-mixin.js @@ -10,10 +10,19 @@ define([ }, updatePrice: function (newPrices) { const res = this._super(newPrices); - if (this._checkIsFinalPriceDefined() && this.element.closest('.product-info-main').length > 0) { + + if (this._checkIsFinalPriceDefined() && + this.element.closest('.product-info-main').length > 0 || + this.element.closest('.bundle-options-container').length > 0) + { containerModel.setCurrentProductId(this.element.data('productId')); containerModel.setPrice(this.cache.displayPrices.finalPrice.amount); + + if(this.element.closest('.bundle-options-container').length > 0 && $("#afterpay-cta-pdp").length) { + $("#afterpay-cta-pdp").attr("data-amount", this.cache.displayPrices.finalPrice.amount); + } } + return res; } }; diff --git a/view/frontend/web/js/view/container/cta/cart/headless.js b/view/frontend/web/js/view/container/cta/cart/headless.js new file mode 100644 index 0000000..32881e8 --- /dev/null +++ b/view/frontend/web/js/view/container/cta/cart/headless.js @@ -0,0 +1,159 @@ +(function (d, w, s) { + let query = `mutation { + getAfterpayConfigCart(input: { + cart_id: "${afterpayCartId}" + store_id: "${afterpayStoreId}" + }) { + allowed_currencies + is_enabled + mpid + is_enabled_cta_cart_page_headless + show_lover_limit + is_product_allowed + is_cbt_enabled + placement_after_selector + price_selector + } + }`; + + let graphqlEndpoint = window.location.origin + '/graphql'; + let configData; + let lastKnownPrice = null; + + function fetchConfigData() { + const requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({query}) + }; + + return fetch(graphqlEndpoint, requestOptions) + .then(response => response.json()) + .then(data => { + if (data) { + const afterpayConfig = data.data.getAfterpayConfigCart; + let isEnabledCtaMinicart = afterpayConfig.is_enabled_cta_cart_page_headless; + + if (afterpayConfig.is_enabled && isEnabledCtaMinicart) { + let dataMPID = afterpayConfig.mpid, + dataShowLowerLimit = afterpayConfig.show_lover_limit, + dataPlatform = 'Magento', + dataPageType = 'cart', + dataIsEligible = afterpayConfig.is_product_allowed, + dataCbtEnabledString = Boolean(afterpayConfig.is_cbt_enabled).toString(), + squarePlacementId = 'afterpay-cta-cart', + widgetContainer = afterpayConfig.placement_after_selector, + priceWrapper = afterpayConfig.price_selector; + + return { + dataShowLowerLimit: dataShowLowerLimit, + dataCurrency: afterpayCurrency, + dataLocale: afterpayLocale, + dataIsEligible: dataIsEligible, + dataMPID: dataMPID, + dataCbtEnabledString: dataCbtEnabledString, + dataPlatform: dataPlatform, + dataPageType: dataPageType, + widgetContainer: widgetContainer, + squarePlacementId: squarePlacementId, + priceWrapper: priceWrapper + }; + } else { + return null; + } + } else { + return null; + } + }) + .catch(error => { + console.error("Error:", error); + throw error; + }); + } + + function processAfterpay() { + if (configData && configData.priceWrapper) { + if (document.querySelector(configData.priceWrapper)) { + updateWidgetInstance(); + lastKnownPrice = getPriceWithoutCurrency(configData.priceWrapper); + setInterval(checkCartUpdated, 1000); + } + } + } + + function updateWidgetInstance() { + const priceWrapper = configData.priceWrapper; + const priceSelectorElement = document.querySelector(priceWrapper); + + if (!priceSelectorElement) { + return; + } + + const squarePlacementSelector = document.getElementById(configData.squarePlacementId); + if (squarePlacementSelector) { + squarePlacementSelector.outerHTML = ""; // Remove old widget and add a new one + } + + let priceAmount = getPriceWithoutCurrency(priceWrapper); + + updateAfterpayAmount(priceAmount); + } + + function checkCartUpdated() { + const currentPrice = getPriceWithoutCurrency(configData.priceWrapper); + if (currentPrice && currentPrice !== lastKnownPrice) { + lastKnownPrice = currentPrice; + updateWidgetInstance(); + } + } + + // Get price without currency symbol + function getPriceWithoutCurrency(selector) { + const element = document.querySelector(selector); + + if (element) { + let priceText = element.innerText.trim(), + price = priceText.replace(/[^\d.]/g, ''); + return parseFloat(price); + } else { + return null; + } + } + + // Add the widget + function updateAfterpayAmount(amount) { + let wrapperHtml = document.querySelector(configData.widgetContainer), + dataCurrency = configData?.dataCurrency ? configData.dataCurrency : window.afterpayCurrency; + + const blockHtml = ''; + + if (wrapperHtml) { + wrapperHtml.insertAdjacentHTML('afterend', blockHtml); + } + } + + window.addEventListener("load", (event) => { + if (afterpayCartId !== "") { + fetchConfigData() + .then(theConfig => { + configData = theConfig; + if (configData && configData.priceWrapper) { + processAfterpay(); + } + }) + .catch(error => console.error("Error: ", error)); + } + }); + +})(document, window, 'script'); diff --git a/view/frontend/web/js/view/container/cta/minicart/headless.js b/view/frontend/web/js/view/container/cta/minicart/headless.js new file mode 100644 index 0000000..acb4ad6 --- /dev/null +++ b/view/frontend/web/js/view/container/cta/minicart/headless.js @@ -0,0 +1,231 @@ +(function (d, w, s) { + let graphqlEndpoint = window.location.origin + '/graphql'; + let configData, + cartId = '', + storeId = ''; + let lastKnownPrice = null; + + function fetchConfigData() { + const query = `mutation { + getAfterpayConfigMiniCart(input: { + cart_id: "${cartId}" + store_id: "${storeId}" + }) { + allowed_currencies + is_enabled + mpid + is_enabled_cta_minicart_headless + show_lover_limit + is_product_allowed + is_cbt_enabled + placement_wrapper + placement_after_selector + price_selector + } + }`; + + const requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query, + variables: {cartId, storeId}, + }) + }; + + return fetch(graphqlEndpoint, requestOptions) + .then(response => response.json()) + .then(data => { + if (data && !data.errors) { + const afterpayConfig = data.data.getAfterpayConfigMiniCart; + let isEnabledCtaMinicart = afterpayConfig.is_enabled_cta_minicart_headless; + + if (afterpayConfig.is_enabled && isEnabledCtaMinicart) { + let dataMPID = afterpayConfig.mpid, + dataShowLowerLimit = afterpayConfig.show_lover_limit, + dataPlatform = 'Magento', + dataPageType = 'mini-cart', + dataIsEligible = afterpayConfig.is_product_allowed, + dataCbtEnabledString = Boolean(afterpayConfig.is_cbt_enabled).toString(), + squarePlacementId = 'afterpay-cta-mini-cart', + minicartBodyWidgetContainer = afterpayConfig.placement_wrapper, + widgetContainer = afterpayConfig.placement_after_selector, + priceWrapper = afterpayConfig.price_selector; + + return { + dataShowLowerLimit: dataShowLowerLimit, + dataCurrency: afterpayCurrency, + dataLocale: afterpayLocale, + dataIsEligible: dataIsEligible, + dataMPID: dataMPID, + dataCbtEnabledString: dataCbtEnabledString, + dataPlatform: dataPlatform, + dataPageType: dataPageType, + minicartBodyWidgetContainer: minicartBodyWidgetContainer, + widgetContainer: widgetContainer, + squarePlacementId: squarePlacementId, + priceWrapper: priceWrapper + }; + } else { + return null; + } + } else { + return null; + } + }) + .catch(error => { + console.error("Error:", error); + throw error; + }); + } + + function processAfterpay() { + if (configData && configData.priceWrapper) { + updateWidgetInstance(); + lastKnownPrice = getPriceWithoutCurrency(configData.priceWrapper); + setInterval(checkCartUpdated, 1000); + } + } + + function observeCacheStorageChanges() { + const initialCacheStorageData = localStorage.getItem('mage-cache-storage'); + + if (initialCacheStorageData) { + const initialCacheStorage = JSON.parse(initialCacheStorageData); + + if (Object.keys(initialCacheStorage).length === 0) { + waitForStorageData(); + } else { + if (initialCacheStorage.cart) { + const initialStoreId = initialCacheStorage.cart.storeId; + let initialCartId = initialCacheStorage.cart.cartId; + + if ((initialCartId !== '') && (initialStoreId !== '')) { + cartId = initialCartId; + storeId = initialStoreId; + callFetchConfigData(); + } else { + waitForStorageData(); + } + } else { + waitForStorageData(); + } + } + } else { + waitForStorageData(); + } + } + + function waitForStorageData() { + let interval = setInterval(function () { + let result = observeLocalStorageEmptyCartChanges(); + if (result) { + clearInterval(interval); + } + }, 1000); + } + + function observeLocalStorageEmptyCartChanges() { + const updatedCacheStorageData = localStorage.getItem('mage-cache-storage'); + + if (updatedCacheStorageData) { + const updatedCacheStorage = JSON.parse(updatedCacheStorageData); + + if (updatedCacheStorage.cart) { + const updatedCartId = updatedCacheStorage.cart.cartId; + const updatedStoreId = updatedCacheStorage.cart.storeId; + + if ((updatedCartId !== '') && (updatedStoreId !== '')) { + cartId = updatedCartId; + storeId = updatedStoreId; + callFetchConfigData(); + + return true; + } + } + } + + return false; + } + + function checkCartUpdated() { + const currentPrice = getPriceWithoutCurrency(configData.priceWrapper); + + if (currentPrice && currentPrice !== lastKnownPrice) { + lastKnownPrice = currentPrice; + + updateWidgetInstance(); + } + } + + function callFetchConfigData() { + if (cartId && storeId) { + fetchConfigData() + .then(theConfig => { + configData = theConfig; + if (configData && configData.priceWrapper) { + processAfterpay(); + } + }) + .catch(error => console.error("Error: ", error)); + } + } + + function updateWidgetInstance() { + const squarePlacementSelector = document.getElementById(configData.squarePlacementId); + + if (squarePlacementSelector) { + squarePlacementSelector.outerHTML = ""; + } + + const cacheStorageData = localStorage.getItem('mage-cache-storage'); + const cacheStorage = JSON.parse(cacheStorageData); + + if (!cacheStorage.cart && !cacheStorage.cart.subtotalAmount) { + return; + } + + const subtotalAmount = cacheStorage.cart.subtotalAmount; + let priceAmount = parseFloat(subtotalAmount); + + updateAfterpayAmount(priceAmount); + } + + function getPriceWithoutCurrency(selector) { + const element = document.querySelector(selector); + + if (element) { + let priceText = element.innerText.trim(), + price = priceText.replace(/[^\d.]/g, ''); + return parseFloat(price); + } else { + return null; + } + } + + function updateAfterpayAmount(amount) { + let wrapperHtml = document.querySelector(configData.widgetContainer), + dataCurrency = configData?.dataCurrency ? configData.dataCurrency : window.afterpayCurrency; + + const blockHtml = ''; + + if (wrapperHtml) { + wrapperHtml.insertAdjacentHTML('afterend', blockHtml); + } + } + + window.addEventListener('load', function() { + observeCacheStorageChanges(); + }); +})(document, window, 'script'); diff --git a/view/frontend/web/js/view/container/cta/pdp/headless.js b/view/frontend/web/js/view/container/cta/pdp/headless.js new file mode 100644 index 0000000..6d9d76b --- /dev/null +++ b/view/frontend/web/js/view/container/cta/pdp/headless.js @@ -0,0 +1,120 @@ +(function (d, w, s) { + let query = `mutation { + getAfterpayConfigPdp(input: { + product_sku: "${afterpayProductSku}" + store_id: "${afterpayStoreId}" + }) { + allowed_currencies + is_enabled + mpid + is_enabled_cta_pdp_headless + product_type + show_lover_limit + is_product_allowed + is_cbt_enabled + placement_after_selector + placement_after_selector_bundle + price_selector + price_selector_bundle + } + }`; + + let graphqlEndpoint = window.location.origin + '/graphql'; + + function fetchConfigData() { + const requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({query}) + }; + + return fetch(graphqlEndpoint, requestOptions) + .then(response => response.json()) + .then(data => { + if (data) { + const afterpayConfig = data.data.getAfterpayConfigPdp; + + if (afterpayConfig.is_enabled && afterpayConfig.is_enabled_cta_pdp_headless) { + let dataMPID = afterpayConfig.mpid, + dataShowLowerLimit = afterpayConfig.show_lover_limit, + dataPlatform = 'Magento', + dataPageType = 'product', + dataIsEligible = afterpayConfig.is_product_allowed, + dataCbtEnabledString = Boolean(afterpayConfig.is_cbt_enabled).toString(), + dataProductType = afterpayConfig.product_type, + squarePlacementId = 'afterpay-cta-pdp', + widgetContainer = afterpayConfig.placement_after_selector, + widgetContainerBundle = afterpayConfig.placement_after_selector_bundle, + priceWrapper = afterpayConfig.price_selector, + priceWrapperBundle = afterpayConfig.price_selector_bundle; + + return { + dataShowLowerLimit: dataShowLowerLimit, + dataCurrency: afterpayCurrency, + dataLocale: afterpayLocale, + dataIsEligible: dataIsEligible, + dataMPID: dataMPID, + dataCbtEnabledString: dataCbtEnabledString, + dataPlatform: dataPlatform, + dataPageType: dataPageType, + dataProductType: dataProductType, + widgetContainer: widgetContainer, + widgetContainerBundle: widgetContainerBundle, + squarePlacementId: squarePlacementId, + priceWrapper: priceWrapper, + priceWrapperBundle: priceWrapperBundle + }; + } else { + return null; + } + } else { + return null; + } + }) + .catch(error => { + console.error("Error:", error); + throw error; + }); + } + + // Process the config data + function processAfterpay() { + fetchConfigData() + .then(configData => { + if (configData && !(configData.dataProductType === 'grouped')) { + updateAfterpayAmount(configData); + } + }) + .catch(error => console.error("Error: ", error)); + } + + // Add the widget + function updateAfterpayAmount(configData) { + let wrapperHtml = document.querySelector(configData.widgetContainer), + priceWrapper = configData.priceWrapper; + + if (configData.dataProductType === 'bundle') { + wrapperHtml = document.querySelector(configData.widgetContainerBundle); + priceWrapper = configData.priceWrapperBundle; + } + + const blockHtml = ''; + + if (wrapperHtml) { + wrapperHtml.insertAdjacentHTML('afterend', blockHtml); + } + } + + processAfterpay(); +})(document, window, 'script'); diff --git a/view/frontend/web/js/view/container/express-checkout/button.js b/view/frontend/web/js/view/container/express-checkout/button.js index bd80613..7f9bfb5 100644 --- a/view/frontend/web/js/view/container/express-checkout/button.js +++ b/view/frontend/web/js/view/container/express-checkout/button.js @@ -53,10 +53,10 @@ define([ if (response && response.afterpay_token) { actions.resolve(response.afterpay_token); } else { - this._fail(actions, AfterPay.constants.SERVICE_UNAVAILABLE); + this._fail(actions, Square.Marketplace.constants.SERVICE_UNAVAILABLE); } }).fail( - () => this._fail(actions, AfterPay.constants.SERVICE_UNAVAILABLE) + () => this._fail(actions, Square.Marketplace.constants.SERVICE_UNAVAILABLE) ); } }, @@ -69,10 +69,10 @@ define([ if (response.success && Array.isArray(response.shippingOptions)) { actions.resolve(response.shippingOptions); } else { - this._fail(actions, AfterPay.constants.SHIPPING_ADDRESS_UNSUPPORTED); + this._fail(actions, Square.Marketplace.constants.SHIPPING_ADDRESS_UNSUPPORTED); } }).fail( - () => this._fail(actions, AfterPay.constants.SHIPPING_ADDRESS_UNRECOGNIZED) + () => this._fail(actions, Square.Marketplace.constants.SHIPPING_ADDRESS_UNRECOGNIZED) ); } }, diff --git a/view/frontend/web/js/view/container/express-checkout/cart/express-checkout-cart-widget.js b/view/frontend/web/js/view/container/express-checkout/cart/express-checkout-cart-widget.js new file mode 100644 index 0000000..3932e27 --- /dev/null +++ b/view/frontend/web/js/view/container/express-checkout/cart/express-checkout-cart-widget.js @@ -0,0 +1,237 @@ +window.addEventListener("load", () => { + const initExpressCheckout = () => { + return { + countryCode: window?.afterpayLocaleCode ? window.afterpayLocaleCode : "US", + enableForCart: false, + isLoading: true, + trigger: "afterpay-button-cart", + minPrice: 0, + maxPrice: 1000, + shippingOptionRequired: true, + isProductAllowed: false, + afterpayCartSubtotal: 0, + ecButtonPlace: document.querySelector(".cart-container .cart-totals"), + wrapElement: document.querySelector("#headless-afterpay-cart-ec"), + isVirtual: false, + + init() { + let self = this; + document.addEventListener('showHeadlessCart', (event) => { + setTimeout(() => { + self.extractSectionData(event.detail.afterpayConfig); + }, 1000); + }); + + document.addEventListener("click", function(e){ + if(e.target.classList.contains('update')) { + setTimeout(function() { + document.dispatchEvent(window.reloadCartPage); + }, 1000); + } + }); + }, + + extractSectionData(data) { + let self = this; + this.isLoading = false; + + this.ecButtonPlace = data?.placement_after_selector + ? document.querySelector(data.placement_after_selector) + : this.ecButtonPlace; + + if (data) { + this.setCurrentData(data); + } + + if (this.ecButtonPlace) { + if (document.querySelector('#afterpay-cta-cart')) { + this.ecButtonPlace = document.querySelector('#afterpay-cta-cart'); + } + let afterpaySection = document.querySelector('.headless-afterpay-cart-ec'); + if(!afterpaySection) { + afterpaySection = self.wrapElement; + } + this.ecButtonPlace.insertAdjacentElement('afterend', afterpaySection); + + this.initAfterpay(); + + // Add click event listener to the button + const afterpayButton = document.querySelector('.afterpay-express-button-cart'); + if (afterpayButton) { + afterpayButton.addEventListener('click', (event) => this.ecValidationAddToCart(event)); + } + + this.validateShowButton(this.checkPriceLimit()); + } + }, + + setCurrentData (data) { + this.enableForCart = (data.is_enabled && data.is_enabled_ec_cart_page_headless) ?? this.enableForPDP; + this.isProductAllowed = data.is_product_allowed ?? this.isProductAllowed; + this.afterpayCartSubtotal = this.checkCurrentSubtotal(); + this.minPrice = data.min_amount ? +data.min_amount : this.minPrice; + this.maxPrice = data.max_amount ? +data.max_amount : this.maxPrice; + this.isVirtual = data.is_virtual ? data.is_virtual : this.isVirtual; + this.shippingOptionRequired = !this.isVirtual; + }, + + checkCurrentSubtotal () { + let currentCartData = JSON.parse(localStorage.getItem("mage-cache-storage")).cart; + + if(currentCartData && currentCartData?.subtotalAmount) { + return +currentCartData?.subtotalAmount; + } + + return 0; + }, + + validateShowButton(priceIsValid = false) { + if (this.enableForCart && this.isProductAllowed && priceIsValid) { + this.wrapElement.classList.remove("hidden"); + } else { + this.wrapElement.classList.add("hidden"); + } + }, + + ecValidationAddToCart(event) { + this.initAfterpay(); + document.getElementById(this.trigger).click(); + }, + + getCurrentSubtotal () { + let currentCartData = JSON.parse(localStorage.getItem("mage-cache-storage"))?.cart; + + if(currentCartData && currentCartData?.subtotalAmount) { + return +currentCartData?.subtotalAmount; + } + + return 0; + }, + + checkPriceLimit() { + let total = this.getCurrentSubtotal(); + + return +total >= this.minPrice && +total <= this.maxPrice; + }, + + objectToUrlEncoded(obj) { + return new URLSearchParams(obj).toString(); + }, + + getShippingOptions(shippingAddress, actions) { + shippingAddress = this.objectToUrlEncoded(shippingAddress); + + this.fetchData("afterpay/express/getShippingOptions", shippingAddress) + .then(response => { + if (response?.shippingOptions) { + return actions.resolve(response.shippingOptions); + } else { + AfterPay.close(); + return actions.reject(Square.Marketplace.constants.SHIPPING_ADDRESS_UNSUPPORTED); + } + }); + }, + + onComplete(event) { + if (event.data.status === 'CANCELLED') { + localStorage?.removeItem('mage-cache-storage'); + window.location.reload(); + } + + this.placeOrder(event); + }, + + handleMessage(type, text) { + if (typeof (dispatchMessages) != "undefined") { + dispatchMessages([{type, text}], 5000); + } + }, + + placeOrder(event) { + const data = this.objectToUrlEncoded(event.data); + + this.isLoading = true; + + this.fetchData("afterpay/express/placeOrder", data) + .then(response => { + if(response?.error) { + let messages = [ + { + text: response?.error, + type: 'error' + } + ], + messagesJson = JSON.stringify(messages); + + cookieStore.set('mage-messages', messagesJson); + window.location.href = response.redirectUrl; + }else{ + if (response?.redirectUrl) { + localStorage?.removeItem('mage-cache-storage'); + localStorage?.removeItem('messages'); + window.mageMessages = []; + window.location.href = response.redirectUrl; + this.isLoading = false; + } + } + }); + }, + + getAfterpayToken(actions) { + this.fetchData("afterpay/express/createCheckout") + .then(response => { + if (response?.afterpay_token) { + return actions.resolve(response.afterpay_token); + } else { + AfterPay.close(); + return actions.reject(Square.Marketplace.constants.SERVICE_UNAVAILABLE); + } + }); + }, + + fetchData(url = "", data = "") { + const postUrl = `${BASE_URL}${url}`; + + this.isLoading = true; + + return window.fetch(postUrl, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest', + }, + body: data, + method: 'POST', + dataType: 'json' + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok ' + response.statusText); + } + return response.json(); + }) + .then(data => { + return data; + }) + .catch(error => { + console.error('There was a problem with the fetch operation:', error); + }); + }, + + initAfterpay() { + AfterPay.initializeForPopup({ + countryCode: this.countryCode.toLocaleUpperCase(), + buyNow: true, + shippingOptionRequired: this.shippingOptionRequired, + pickup: false, + target: "#" + this.trigger, + onCommenceCheckout: actions => this.getAfterpayToken(actions), + onComplete: event => this.onComplete(event), + onShippingAddressChange: (shippingAddress, actions) => this.getShippingOptions(shippingAddress, actions) + }); + } + }; + }; + + window.expressCheckout = initExpressCheckout(); + window.expressCheckout.init(); +}); diff --git a/view/frontend/web/js/view/container/express-checkout/cart/headless.js b/view/frontend/web/js/view/container/express-checkout/cart/headless.js new file mode 100644 index 0000000..d253c96 --- /dev/null +++ b/view/frontend/web/js/view/container/express-checkout/cart/headless.js @@ -0,0 +1,72 @@ +(function (d, w, s) { + let query = `mutation { + getAfterpayConfigCart(input: { + cart_id: "${afterpayCartId}" + store_id: "${afterpayStoreId}" + }) { + allowed_currencies + is_enabled + mpid + is_enabled_ec_cart_page_headless + show_lover_limit + is_product_allowed + is_cbt_enabled + placement_after_selector + price_selector + max_amount + min_amount + is_virtual + } + }`; + + let graphqlEndpoint = window.location.origin + '/graphql'; + let configData; + + function fetchConfigData() { + const requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({query}) + }; + + return fetch(graphqlEndpoint, requestOptions) + .then(response => response.json()) + .then(data => { + if (data) { + const afterpayConfig = data.data.getAfterpayConfigCart; + + if (afterpayConfig) { + const event = new CustomEvent('showHeadlessCart', {detail: {afterpayConfig}}); + document.dispatchEvent(event); + } else { + return null; + } + } else { + return null; + } + }) + .catch(error => { + console.error("Error:", error); + throw error; + }); + } + + window.addEventListener("load", (event) => { + if (afterpayCartId !== "") { + fetchConfigData(); + } + }); + + // Create the custom event + window.reloadCartPage = new CustomEvent('reloadCartPage'); + + // Attach an event listener + document.addEventListener('reloadCartPage', function() { + if (afterpayCartId !== "") { + fetchConfigData(); + } + }); + +})(document, window, 'script'); diff --git a/view/frontend/web/js/view/container/express-checkout/check-message.js b/view/frontend/web/js/view/container/express-checkout/check-message.js new file mode 100644 index 0000000..11f1245 --- /dev/null +++ b/view/frontend/web/js/view/container/express-checkout/check-message.js @@ -0,0 +1,37 @@ +window.addEventListener("load", (event) => { + + let url = window.location.href; + + if (url.includes('success')) { + const handleMessages = () => { + let messages = document.querySelectorAll(".message.success"); + + messages?.forEach((message) => { + let links = message.getElementsByTagName('a'); + + if(links.length > 0) { + for (let link of links) { + if (link.href.includes("checkout/cart")) { + message.remove(); + break; + } + } + }else { + message.classList.add("show-message"); + } + }); + }; + + handleMessages(); + + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.addedNodes.length > 0) { + handleMessages(); + } + }); + }); + + observer.observe(document.body, { childList: true, subtree: true }); + } +}); diff --git a/view/frontend/web/js/view/container/express-checkout/minicart/express-checkout-minicart-widget.js b/view/frontend/web/js/view/container/express-checkout/minicart/express-checkout-minicart-widget.js new file mode 100644 index 0000000..0ff084f --- /dev/null +++ b/view/frontend/web/js/view/container/express-checkout/minicart/express-checkout-minicart-widget.js @@ -0,0 +1,309 @@ +window.addEventListener("load", () => { + const initExpressCheckout = () => { + return { + countryCode: window?.afterpayLocaleCode ? window.afterpayLocaleCode : "US", + enableForMinicart: false, + isLoading: true, + trigger: "afterpay-button-minicart", + minPrice: 0, + maxPrice: 1000, + shippingOptionRequired: true, + isProductAllowed: false, + afterpayCartSubtotal: 0, + ecButtonPlace: document.querySelector("#minicart-content-wrapper .subtotal"), + configData: '', + afterpayButton: document.querySelector('#headless-afterpay-minicart-ec'), + initAfterpayAction: false, + isVirtual: false, + + init() { + document.addEventListener('showHeadlessMinicart', (event) => { + this.extractSectionData(event.detail.afterpayConfig); + this.configData = event.detail.afterpayConfig; + }); + }, + + extractSectionData(data, observer = false) { + this.isLoading = false; + + this.ecButtonPlace = data?.placement_after_selector + ? document.querySelector(data.placement_after_selector) + : this.ecButtonPlace; + + if(!observer) { + this.trackPriceChanges(document.querySelector(data.placement_wrapper)); + } + + if (data) { + this.setCurrentData(data); + } + + if (this.ecButtonPlace) { + if (document.querySelector('#afterpay-cta-mini-cart')) { + this.ecButtonPlace = document.querySelector('#afterpay-cta-mini-cart'); + } + + this.ecButtonPlace.insertAdjacentElement('afterend', this.afterpayButton); + + if(!this.initAfterpayAction) { + this.initAfterpay(); + } + + // Add click event listener to the button + if (this.afterpayButton) { + this.afterpayButton.addEventListener('click', (event) => this.ecValidationAddToCart(event)); + } + + if(document.querySelector(".product-info-main #product-addtocart-button")){ + document.querySelector(".product-info-main #product-addtocart-button").addEventListener('click', (event) => + this.trackPriceChanges(document.querySelector(this.configData.placement_after_selector)) + ); + } + + this.validateShowButton(this.checkPriceLimit(this.afterpayCartSubtotal)); + } + }, + + setCurrentData (data) { + this.enableForMinicart = (data.is_enabled && data.is_enabled_ec_minicart_headless) ?? this.enableForPDP; + this.isProductAllowed = data.is_product_allowed ?? this.isProductAllowed; + this.afterpayCartSubtotal = this.checkCurrentSubtotal(); + this.minPrice = data.min_amount ? +data.min_amount : this.minPrice; + this.maxPrice = data.max_amount ? +data.max_amount : this.maxPrice; + this.isVirtual = data.is_virtual ? data.is_virtual : this.isVirtual; + this.shippingOptionRequired = !this.isVirtual; + window.miniCartHasVirtual = this.isVirtual; + }, + + checkCurrentSubtotal () { + let currentCartData = JSON.parse(localStorage.getItem("mage-cache-storage")).cart; + + if(currentCartData && currentCartData?.subtotalAmount) { + return +currentCartData?.subtotalAmount; + } + + return 0; + }, + + reloadMinicart () { + const url = '/customer/section/load/'; + fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: {sections: "cart"} + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .catch(error => { + console.error('Failed to refresh cart section:', error); + }); + }, + + trackPriceChanges(element) { + if(!element) return; + + if (document.querySelector(this.configData.placement_after_selector)) { + this.extractSectionData(this.configData, true); + return; + } + + const targetNode = element, + callback = (mutationsList, observer) => { + for (const mutation of mutationsList) { + mutation.addedNodes.forEach(node => { + if (node?.classList?.value == 'price' && document.querySelector(this.configData.placement_after_selector)) { + document.dispatchEvent(window.reloadMinicart); + // observer.disconnect(); + return; + } + }); + } + }; + + const observer = new MutationObserver(callback), + config = { + characterData: true, + childList: true, + subtree: true + }; + + observer.observe(targetNode, config); + }, + + validateShowButton(priceIsValid = false) { + if (this.enableForMinicart && this.isProductAllowed && priceIsValid) { + this.afterpayButton.classList.remove("hidden"); + } else { + this.afterpayButton.classList.add("hidden"); + } + }, + + ecValidationAddToCart(event) { + document.getElementById(this.trigger).click(); + this.initAfterpay(); + }, + + checkPriceLimit(cartSubtotal) { + let total = cartSubtotal?.subtotalAmount ? cartSubtotal?.subtotalAmount : cartSubtotal; + + return +total >= this.minPrice && +total <= this.maxPrice; + }, + + objectToUrlEncoded(obj) { + return new URLSearchParams(obj).toString(); + }, + + getShippingOptions(shippingAddress, actions) { + shippingAddress = this.objectToUrlEncoded(shippingAddress); + + this.fetchData("afterpay/express/getShippingOptions", shippingAddress) + .then(response => { + if (response?.shippingOptions) { + return actions.resolve(response.shippingOptions); + } else { + AfterPay.close(); + return actions.reject(Square.Marketplace.constants.SHIPPING_ADDRESS_UNSUPPORTED); + } + }); + }, + + onComplete(event) { + if (event.data.status === 'CANCELLED') { + localStorage?.removeItem('mage-cache-storage'); + window.location.reload(); + } + + this.placeOrder(event); + }, + + handleMessage(type, text) { + if (typeof (dispatchMessages) != "undefined") { + dispatchMessages([{type, text}], 5000); + } + }, + + placeOrder(event) { + const data = this.objectToUrlEncoded(event.data); + + this.isLoading = true; + + this.fetchData("afterpay/express/placeOrder", data) + .then(response => { + if(response?.error) { + let messages = [ + { + text: response?.error, + type: 'error' + } + ], + messagesJson = JSON.stringify(messages); + + cookieStore.set('mage-messages', messagesJson); + window.location.href = response.redirectUrl; + }else{ + if (response?.redirectUrl) { + localStorage?.removeItem('mage-cache-storage'); + localStorage?.removeItem('messages'); + window.mageMessages = []; + window.location.href = response.redirectUrl; + this.isLoading = false; + } + } + }); + }, + + getAfterpayToken(actions) { + this.fetchData("afterpay/express/createCheckout") + .then(response => { + if (response?.afterpay_token) { + return actions.resolve(response.afterpay_token); + } else { + AfterPay.close(); + return actions.reject(Square.Marketplace.constants.SERVICE_UNAVAILABLE); + } + }); + }, + + fetchData(url = "", data = "") { + const postUrl = `${BASE_URL}${url}`; + + this.isLoading = true; + + return window.fetch(postUrl, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest', + }, + body: data, + method: 'POST', + dataType: 'json' + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok ' + response.statusText); + } + return response.json(); + }) + .then(data => { + return data; + }) + .catch(error => { + console.error('There was a problem with the fetch operation:', error); + }); + }, + + checkProductInCart () { + let cartItems = JSON.parse(localStorage.getItem("mage-cache-storage"))?.cart?.items, + hasVirtual = false, + hasSimple = false; + + if(cartItems?.length > 0) { + cartItems.forEach((item, index) => { + if(item.product_type == "virtual" || item.product_type == "downloadable") { + hasVirtual = true; + }else { + hasSimple = true; + } + }); + } + + if(hasVirtual && hasSimple) { + this.shippingOptionRequired = true; + } + + if(hasVirtual && hasSimple == false) { + this.shippingOptionRequired = false; + } + + if(hasVirtual == false && hasSimple) { + this.shippingOptionRequired = true; + } + }, + + initAfterpay() { + this.checkProductInCart(); + + AfterPay.initializeForPopup({ + countryCode: this.countryCode.toLocaleUpperCase(), + buyNow: true, + shippingOptionRequired: this.shippingOptionRequired, + pickup: false, + target: "#" + this.trigger, + onCommenceCheckout: actions => this.getAfterpayToken(actions), + onComplete: event => this.onComplete(event), + onShippingAddressChange: (shippingAddress, actions) => this.getShippingOptions(shippingAddress, actions) + }); + this.initAfterpayAction = true; + } + }; + }; + + window.expressCheckout = initExpressCheckout(); + window.expressCheckout.init(); +}); diff --git a/view/frontend/web/js/view/container/express-checkout/minicart/headless.js b/view/frontend/web/js/view/container/express-checkout/minicart/headless.js new file mode 100644 index 0000000..35632de --- /dev/null +++ b/view/frontend/web/js/view/container/express-checkout/minicart/headless.js @@ -0,0 +1,138 @@ +(function (d, w, s) { + let graphqlEndpoint = window.location.origin + '/graphql'; + let configData, + cartId = '', + storeId = ''; + let lastKnownPrice = null; + + function fetchConfigData() { + const query = `mutation { + getAfterpayConfigMiniCart(input: { + cart_id: "${cartId}" + store_id: "${storeId}" + }) { + allowed_currencies + is_enabled + mpid + is_enabled_ec_minicart_headless + show_lover_limit + is_product_allowed + is_cbt_enabled + placement_wrapper + placement_after_selector + price_selector + max_amount + min_amount + is_virtual + } + }`; + + const requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query, + variables: {cartId, storeId}, + }) + }; + + return fetch(graphqlEndpoint, requestOptions) + .then(response => response.json()) + .then(data => { + if (data && !data.errors) { + const afterpayConfig = data.data.getAfterpayConfigMiniCart; + + if (afterpayConfig) { + const event = new CustomEvent('showHeadlessMinicart', {detail: {afterpayConfig}}); + document.dispatchEvent(event); + } else { + return null; + } + } else { + return null; + } + }) + .catch(error => { + console.error("Error:", error); + throw error; + }); + } + + function observeCacheStorageChanges() { + const initialCacheStorageData = localStorage.getItem('mage-cache-storage'); + + if (initialCacheStorageData) { + const initialCacheStorage = JSON.parse(initialCacheStorageData); + + if (Object.keys(initialCacheStorage).length === 0) { + waitForStorageData(); + } else { + if (initialCacheStorage.cart) { + const initialStoreId = initialCacheStorage.cart.storeId; + let initialCartId = initialCacheStorage.cart.cartId; + + if ((initialCartId !== '') && (initialStoreId !== '')) { + cartId = initialCartId; + storeId = initialStoreId; + fetchConfigData(); + } else { + waitForStorageData(); + } + } else { + waitForStorageData(); + } + } + } else { + waitForStorageData(); + } + } + + function waitForStorageData() { + let interval = setInterval(function () { + let result = observeLocalStorageEmptyCartChanges(); + if (result) { + clearInterval(interval); + } + }, 1000); + } + + function observeLocalStorageEmptyCartChanges() { + const updatedCacheStorageData = localStorage.getItem('mage-cache-storage'); + + if (updatedCacheStorageData) { + const updatedCacheStorage = JSON.parse(updatedCacheStorageData); + + if (updatedCacheStorage.cart) { + const updatedCartId = updatedCacheStorage.cart.cartId; + const updatedStoreId = updatedCacheStorage.cart.storeId; + + if ((updatedCartId !== '') && (updatedStoreId !== '')) { + cartId = updatedCartId; + storeId = updatedStoreId; + fetchConfigData(); + + return true; + } + } + } + + return false; + } + + window.addEventListener('load', function () { + observeCacheStorageChanges(); + }); + + // Create the custom event + window.reloadMinicart = new CustomEvent('reloadMinicart'); + + // Attach an event listener + document.addEventListener('reloadMinicart', function() { + if(cartId && storeId) { + fetchConfigData(); + } + }); + +})(document, window, 'script'); diff --git a/view/frontend/web/js/view/container/express-checkout/product/button.js b/view/frontend/web/js/view/container/express-checkout/product/button.js index c75a1fa..fbeeb63 100644 --- a/view/frontend/web/js/view/container/express-checkout/product/button.js +++ b/view/frontend/web/js/view/container/express-checkout/product/button.js @@ -47,7 +47,7 @@ define([ } this.onCartUpdated = $.Deferred(); this.onCartUpdated.done(() => parentOnCommenceCheckoutAfterpayMethod(actions)) - .fail(() => this._fail(actions, AfterPay.constants.SERVICE_UNAVAILABLE)); + .fail(() => this._fail(actions, Square.Marketplace.constants.SERVICE_UNAVAILABLE)); } }, _getOnComplete: function () { diff --git a/view/frontend/web/js/view/container/express-checkout/product/express-checkout-widget.js b/view/frontend/web/js/view/container/express-checkout/product/express-checkout-widget.js new file mode 100644 index 0000000..8fdeb00 --- /dev/null +++ b/view/frontend/web/js/view/container/express-checkout/product/express-checkout-widget.js @@ -0,0 +1,375 @@ +'use strict'; + +window.addEventListener("load", () => { + const initExpressCheckout = () => { + return { + countryCode: window?.afterpayLocaleCode ? window.afterpayLocaleCode : "US", + enableForPDP: false, + isLoading: true, + trigger: "afterpay-button-pdp", + minPrice: 0, + maxPrice: 1000, + priceSelector: ".product-info-main .price-final_price .price-wrapper .price", + shippingOptionRequired: true, + isProductAllowed: false, + afterpayCartSubtotal: 0, + ecButtonPlace: document.querySelector("#product_addtocart_form"), + wrapElement: document.querySelector("#headless-afterpay-pdp-ec"), + configData: '', + + init() { + document.addEventListener('showHeadlessEC', (event) => { + this.extractSectionData(event.detail.afterpayConfig); + this.configData = event.detail.afterpayConfig; + }); + }, + + extractSectionData(data) { + this.isLoading = false; + + this.ecButtonPlace = data?.placement_after_selector && + data?.placement_after_selector_bundle && + data?.product_type !== "bundle" ? + document.querySelector(data.placement_after_selector) : + document.querySelector(data.placement_after_selector_bundle); + + if (data) { + this.setCurrentData(data); + } + + if (this.ecButtonPlace) { + if (document.querySelector('#afterpay-cta-pdp')) { + this.ecButtonPlace = document.querySelector('#afterpay-cta-pdp'); + } + + this.validateShowButton(); + const afterpaySection = document.querySelector('.headless-afterpay-pdp-ec'); + this.ecButtonPlace.insertAdjacentElement('afterend', afterpaySection); + + // Add click event listener to the button + const afterpayButton = document.querySelector('.afterpay-express-pdp-button'); + if (afterpayButton) { + afterpayButton.addEventListener('click', (event) => this.ecValidationAddToCart(event)); + } + } + }, + + setCurrentData (data) { + this.shippingOptionRequired = data.product_type !== "virtual" && data.product_type !== "downloadable"; + this.minPrice = data.min_amount ? +data.min_amount : this.minPrice; + this.maxPrice = data.max_amount ? +data.max_amount : this.maxPrice; + this.enableForPDP = (data.is_enabled && data.is_enabled_ec_pdp_headless) ?? this.enableForPDP; + this.isProductAllowed = data.is_product_allowed ?? this.isProductAllowed; + this.afterpayCartSubtotal = this.checkCurrentSubtotal(); + this.priceSelector = data?.product_type != "bundle" ? data?.price_selector : data?.price_selector_bundle; + let element = this.priceSelector ? document.querySelector(this.priceSelector).closest(".price-wrapper") : ''; + this.trackPriceChanges(element); + this.checkPriceLimit(this.afterpayCartSubtotal); + }, + + checkCurrentSubtotal () { + let currentCartData = JSON.parse(localStorage.getItem("mage-cache-storage"))?.cart; + + if(currentCartData && currentCartData?.subtotalAmount) { + return +currentCartData?.subtotalAmount; + } + + return 0; + }, + + validateShowButton() { + let currentPrice = this.getCurrentPrice(); + + if (this.enableForPDP && this.isProductAllowed && +currentPrice >= +this.minPrice && +currentPrice <= +this.maxPrice) { + this.wrapElement.classList.remove("hidden"); + } else { + this.wrapElement.classList.add("hidden"); + } + }, + + getCurrentPrice() { + let currentPrice = document.querySelector(this.priceSelector).textContent; + currentPrice = currentPrice.replace(/[^\d.]/g, ''); + + return currentPrice; + }, + + getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + let form_key = ""; + + if (parts.length === 2) { + form_key = parts.pop().split(';').shift(); + } else { + if (parts.length > 2) + form_key = parts[1].split(';')[0] + } + + return form_key; + }, + + addToCart(isValid) { + const postUrl = `${BASE_URL}checkout/cart/add/`; + const form = document.forms.product_addtocart_form; + + if (!isValid) return; + + const formData = new FormData(form); + formData.append('form_key', this.getCookie('form_key')); + + this.isLoading = true; + + window.fetch(postUrl, { + body: formData, + method: 'POST' + }) + .catch(console.error) + .finally(() => { + addEventListener('private-content-loaded', event => { + if (this.checkPriceLimit(event.detail.data.cart)) { + document.getElementById(this.trigger).click(); + } + }); + + dispatchEvent(new Event('reload-customer-section-data')); + this.initAfterpay(); + document.getElementById(this.trigger).click(); + }); + }, + + ecValidationAddToCart(event) { + event.stopImmediatePropagation(); + + const form = document.forms.product_addtocart_form; + let isValid = form?.reportValidity(); + + if (form && typeof (require) != "undefined") { + require([ + 'jquery', + 'mage/mage' + ], function ($) { + + let dataForm = $('#product_addtocart_form'), + isValid = false; + + if (dataForm.valid()) { + isValid = true; + } + + const event = new CustomEvent('ecFormValid', {detail: {isValid: isValid}}); + document.dispatchEvent(event); + + }); + + document.addEventListener('ecFormValid', (event) => { + if (event?.detail?.isValid) { + if (this.configData?.product_type == "bundle") { + this.initAfterpay(); + setTimeout(() => { + document.getElementById(this.trigger).click(); + }, 1000); + }else{ + this.addToCart(event.detail.isValid); + } + } + }); + } else { + if (form || form?.reportValidity()) { + this.addToCart(isValid); + } + } + }, + + trackPriceChanges(element) { + if(!element) return; + + const targetNode = element, + callback = (mutationsList, observer) => { + for (const mutation of mutationsList) { + if (mutation.type === 'characterData' || mutation.type === 'childList') { + this.validateShowButton(); + } + } + }; + + const observer = new MutationObserver(callback), + config = { + characterData: true, + childList: true, + subtree: true + }; + + observer.observe(targetNode, config); + }, + + checkPriceLimit(cartSubtotal) { + let total = cartSubtotal?.subtotalAmount ? cartSubtotal?.subtotalAmount : cartSubtotal, + currentPrice = this.getCurrentPrice(); + + if (+currentPrice >= +this.minPrice && +total <= this.maxPrice) { + this.wrapElement.classList.remove("hidden"); + return true; + } else { + this.wrapElement.classList.add("hidden"); + } + + this.isLoading = false; + return false; + }, + + objectToUrlEncoded(obj) { + return new URLSearchParams(obj).toString(); + }, + + getShippingOptions(shippingAddress, actions) { + shippingAddress = this.objectToUrlEncoded(shippingAddress); + + this.fetchData("afterpay/express/getShippingOptions", shippingAddress) + .then(response => { + if (response?.shippingOptions) { + return actions.resolve(response.shippingOptions); + } else { + AfterPay.close(); + return actions.reject(Square.Marketplace.constants.SHIPPING_ADDRESS_UNSUPPORTED); + } + }); + }, + + onComplete(event) { + if (event.data.status === 'CANCELLED') { + localStorage?.removeItem('mage-cache-storage'); + window.location.reload(); + } + + this.placeOrder(event); + }, + + handleMessage(type, text) { + if (typeof (dispatchMessages) != "undefined") { + dispatchMessages([{type, text}], 5000); + } + }, + + placeOrder(event) { + const data = this.objectToUrlEncoded(event.data); + + this.isLoading = true; + + this.fetchData("afterpay/express/placeOrder", data) + .then(response => { + if(response?.error) { + let messages = [ + { + text: response?.error, + type: 'error' + } + ], + messagesJson = JSON.stringify(messages); + + cookieStore.set('mage-messages', messagesJson); + window.location.href = response.redirectUrl; + }else{ + if (response?.redirectUrl) { + localStorage?.removeItem('mage-cache-storage'); + localStorage?.removeItem('messages'); + window.mageMessages = []; + window.location.href = response.redirectUrl; + this.isLoading = false; + } + } + }); + }, + + getAfterpayToken(actions) { + this.fetchData("afterpay/express/createCheckout") + .then(response => { + if (response?.afterpay_token) { + return actions.resolve(response.afterpay_token); + } else { + AfterPay.close(); + return actions.reject(Square.Marketplace.constants.SERVICE_UNAVAILABLE); + } + }); + }, + + fetchData(url = "", data = "") { + const postUrl = `${BASE_URL}${url}`; + + this.isLoading = true; + + return window.fetch(postUrl, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest', + }, + body: data, + method: 'POST', + dataType: 'json' + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok ' + response.statusText); + } + return response.json(); + }) + .then(data => { + return data; + }) + .catch(error => { + console.error('There was a problem with the fetch operation:', error); + }); + }, + + checkProductInCart () { + let cartItems = JSON.parse(localStorage.getItem("mage-cache-storage"))?.cart?.items, + hasVirtual = false, + hasSimple = false; + + if(cartItems?.length > 0) { + cartItems.forEach((item, index) => { + if(item.product_type == "virtual" || item.product_type == "downloadable") { + hasVirtual = true; + }else { + hasSimple = true; + } + }); + } + + if(hasVirtual && hasSimple) { + this.shippingOptionRequired = true; + } + + if(hasVirtual && hasSimple == false) { + if(this.configData.product_type !== "virtual" && this.configData.product_type !== "downloadable") { + this.shippingOptionRequired = true; + } + } + + if(hasVirtual == false && hasSimple) { + if(this.configData.product_type == "virtual" || this.configData.product_type == "downloadable") { + this.shippingOptionRequired = true; + } + } + }, + + initAfterpay() { + this.checkProductInCart(); + + AfterPay.initializeForPopup({ + countryCode: this.countryCode.toLocaleUpperCase(), + buyNow: true, + shippingOptionRequired: this.shippingOptionRequired, + pickup: false, + target: "#" + this.trigger, + onCommenceCheckout: actions => this.getAfterpayToken(actions), + onComplete: event => this.onComplete(event), + onShippingAddressChange: (shippingAddress, actions) => this.getShippingOptions(shippingAddress, actions) + }); + } + }; + }; + + window.expressCheckout = initExpressCheckout(); + window.expressCheckout.init(); +}); diff --git a/view/frontend/web/js/view/container/express-checkout/product/headless.js b/view/frontend/web/js/view/container/express-checkout/product/headless.js new file mode 100644 index 0000000..aed04a7 --- /dev/null +++ b/view/frontend/web/js/view/container/express-checkout/product/headless.js @@ -0,0 +1,65 @@ +(function (d, w, s) { + let query = `mutation { + getAfterpayConfigPdp(input: { + product_sku: "${afterpayProductSku}" + store_id: "${afterpayStoreId}" + }) { + allowed_currencies + is_enabled + mpid + is_enabled_ec_pdp_headless + product_type + show_lover_limit + is_product_allowed + placement_after_selector + placement_after_selector_bundle + is_cbt_enabled + max_amount + min_amount + price_selector + price_selector_bundle + } + }`; + + let graphqlEndpoint = window.location.origin + '/graphql'; + + function fetchConfigData() { + const requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({query}) + }; + + return fetch(graphqlEndpoint, requestOptions) + .then(response => response.json()) + .then(data => { + if (data) { + if(data.errors) { + console.error("Error:", data.errors[0].message); + return null; + } + const afterpayConfig = data.data.getAfterpayConfigPdp; + + if(afterpayConfig) { + const event = new CustomEvent('showHeadlessEC', { detail: { afterpayConfig} }); + document.dispatchEvent(event); + } + } else { + return null; + } + }) + .catch(error => { + console.error("Error:", error); + throw error; + }); + } + + window.addEventListener("load", (event) => { + if (afterpayProductSku !== "") { + fetchConfigData(); + } + }); + +})(document, window, 'script');