diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28053aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/app/etc/local.xml + +/errors/local.xml + +/details5.php +/var +/media +/details5.php +/details5.php + +/app/code/community/Fera/Aiconnector/CustomerBalance/license.txt +/.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2712b6 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Fera.ai Magento 2 Extension +This extension makes it easy to use Fera.ai to offer customers a live shopping experience in your product pages. To learn more about Fera go to https://www.fera.ai + + +## Installation +### 1. Create An Account +Go to https://app.fera.ai/signup?platform=magento and create a new account. + +### 2. Follow the instructions to install the extension. +You will be provided with a link to the latest install files. Follow the instructions to install the app into your Magento store. + +*That's it!*. If you follow the instructions described then your store will automatically be configured to connected to the Fera.ai servers. + +## Usage +Go to https://app.fera.ai/skills to customize your experience! + +## Help +If you're seeing this repo you're probably a trusted developer - so just feel free to email help a-t fera dot ai with any questions. diff --git a/src/app/code/Fera/Ai/Block/Footer.php b/src/app/code/Fera/Ai/Block/Footer.php new file mode 100644 index 0000000..7b081b5 --- /dev/null +++ b/src/app/code/Fera/Ai/Block/Footer.php @@ -0,0 +1,98 @@ +customerSession = $customerSession; + $this->helper = $helper; + parent::__construct($context, $data); + } + + public function isEnabled() + { + return $this->helper->isEnabled(); + } + + public function getDebugJs() + { + return $this->helper->getDebugJs(); + } + + public function getPublicKey() + { + return $this->helper->getPublicKey(); + } + + public function getApiUrl() + { + return $this->helper->getApiUrl(); + } + + public function getJsUrl() + { + return $this->helper->getJsUrl(); + } + + public function getShopperData() + { + $customer = $this->customerSession->getCustomer(); + + if (!$customer->getId()) return false; + + $shopperData = [ + 'customer_id' => $customer->getId(), + 'email' => $customer->getEmail(), + 'name' => $customer->getName(), + ]; + + return $this->helper->jsonEncode($shopperData); + } + + public function getCartJson() + { + return $this->helper->getCartJson(); + } + + public function jsonEncode($data) + { + return $this->helper->jsonEncode($data); + } +} diff --git a/src/app/code/Fera/Ai/Block/Footer/Checkout/Success.php b/src/app/code/Fera/Ai/Block/Footer/Checkout/Success.php new file mode 100644 index 0000000..a9e8e05 --- /dev/null +++ b/src/app/code/Fera/Ai/Block/Footer/Checkout/Success.php @@ -0,0 +1,119 @@ +customerSession = $customerSession; + $this->directoryHelper = $directoryHelper; + $this->helper = $helper; + parent::__construct($context, $checkoutSession, $orderConfig, $httpContext, $data); + } + + /** + * @return bool|mixed + */ + public function isEnabled() + { + return $this->helper->isEnabled(); + } + + /** + * Get last order, JSONify it and return it. + * @return String + */ + public function getOrderJson() + { + $order = $this->_checkoutSession->getLastRealOrder(); + + $customer = null; + if (!empty($this->customerSession->getCustomerId())) { + $customer = $this->customerSession->getCustomer(); + $address = $customer->getDefaultShippingAddress(); + + if (!empty($address)) { + $address = $address->getData(); + } + + $customer = [ + 'id' => $this->customerSession->getCustomerId(), + 'first_name' => $this->customerSession->getCustomer()->getFirstname(), + 'email' => $this->customerSession->getCustomer()->getEmail(), + 'address' => $address + ]; + } + + $currencyCode = $this->_storeManager->getStore()->getCurrentCurrencyCode(); + $total = $order->getGrandTotal(); + $totalUsd = $this->directoryHelper->currencyConvert($total, $currencyCode, 'USD'); + + $orderData = [ + 'id' => $order->getId(), + 'number' => $order->getIncrementId(), + 'total' => $total, + 'total_usd' => $totalUsd, + 'created_at' => $this->helper->formatDate($order->getCreatedAt()), + 'modified_at' => $this->helper->formatDate($order->getUpdatedAt()), + 'line_items' => $this->helper->serializeQuoteItems($order->getAllItems()), + 'customer' => $customer, + 'source_name' => 'web' + ]; + + return $this->helper->jsonEncode($orderData); + } + +} diff --git a/src/app/code/Fera/Ai/Block/Footer/Product/View.php b/src/app/code/Fera/Ai/Block/Footer/Product/View.php new file mode 100644 index 0000000..55885a1 --- /dev/null +++ b/src/app/code/Fera/Ai/Block/Footer/Product/View.php @@ -0,0 +1,166 @@ +stockState = $stockState; + $this->helper = $helper; + parent::__construct($context, $urlEncoder, $jsonEncoder, $string, $productHelper, $productTypeConfig, $localeFormat, $customerSession, $productRepository, $priceCurrency, $data); + } + + /** + * @return bool|mixed + */ + public function isEnabled() + { + return $this->helper->isEnabled(); + } + + /** + * Get product, JSONify it and return it. + * @return string + */ + public function getProductJson() + { + $p = $this->getProduct(); + $thumb = $this->helper->getProductThumbnailUrl($p); + + $productData = [ + "id" => $p->getId(), // String + "name" => $p->getName(), // String + "price" => $p->getFinalPrice(), // Float + "status" => $p->getStatus() == 1 ? 'published' : 'draft', // (Optional) String + "created_at" => $this->helper->formatDate($p->getCreatedAt()), // (Optional) String (ISO 8601 format DateTime) + "modified_at" => $this->helper->formatDate($p->getUpdatedAt()), // (Optional) String (ISO 8601 format DateTime) + "stock" => $this->stockState->getStockQty($p->getId(), $p->getStore()->getWebsiteId()), // (Optional) Integer, If null assumed to be infinite. + "in_stock" => $p->isInStock(), // (Optional) Boolean + "url" => $p->getProductUrl(), // String + "thumbnail_url" => $thumb, // String + "needs_shipping" => $p->getTypeId() != 'virtual', // (Optional) Boolean + "hidden" => $p->getVisibility() == '1', // (Optional) Boolean + 'tags' => [], // M2 not included tags by default + "variants" => [], // (Optional) Array: Variants that are applicable to this product. + "platform_data" => [ // (Optional) Hash/Object of attributes to store about the product specific to the integration platform (can be used in future filters) + "sku" => $p->getSku(), + "type" => $p->getTypeId(), + "regular_price" => $p->getPrice() + ] + ]; + + if ($p->getTypeId() == 'configurable') { + $cfgAttr = $p->getTypeInstance()->getConfigurableAttributesAsArray($p); + + foreach ($p->getTypeInstance()->getUsedProducts($p) as $subProduct) { + + $variant = [ + "id" => $subProduct->getId(), + "name" => $subProduct->getName(), // String + "status" => $subProduct->getStatus() == 1 ? 'published' : 'draft', // (Optional) String + "created_at" => $this->helper->formatDate($subProduct->getCreatedAt()), // (Optional) String (ISO 8601 format DateTime) + "modified_at" => $this->helper->formatDate($subProduct->getUpdatedAt()), // (Optional) String (ISO 8601 format DateTime) + "stock" => $this->stockState->getStockQty($subProduct->getId(), $subProduct->getStore()->getWebsiteId()), // (Optional) Integer, If null assumed to be infinite. + "in_stock" => $subProduct->isInStock(), // (Optional) Boolean + "price" => $subProduct->getPrice(), // Float + "platform_data" => [ // (Optional) Hash/Object of attributes to store about the product specific to the integration platform (can be used in future filters) + "sku" => $subProduct->getSku() + ] + ]; + + $variantImage = $this->helper->getProductThumbnailUrl($subProduct); + if ($variantImage != $thumb && stripos($variantImage, '/placeholder') === false){ + $variant['thumbnail_url'] = $variantImage; + } + + $variantAttrVals = []; + foreach ($cfgAttr as $attr) { + $attrValIndex = $subProduct->getData($attr['attribute_code']); + foreach ($attr['values'] as $attrVal) { + if ($attrVal['value_index'] == $attrValIndex) { + $variantAttrVals[] = $attrVal['label']; + } + } + } + $variant['name'] = implode(' / ', $variantAttrVals); + + $productData['variants'][] = $variant; + } + } + + return $this->helper->jsonEncode($productData); + } + + /** + * @return int + */ + public function getProductId() + { + return $this->getProduct()->getId(); + } + +} diff --git a/src/app/code/Fera/Ai/Helper/Data.php b/src/app/code/Fera/Ai/Helper/Data.php new file mode 100644 index 0000000..e5b9151 --- /dev/null +++ b/src/app/code/Fera/Ai/Helper/Data.php @@ -0,0 +1,273 @@ +moduleResource = $moduleResource; + $this->storeManager = $storeManager; + $this->jsonHelper = $jsonHelper; + $this->checkoutSession = $checkoutSession; + $this->dateTime = $dateTime; + $this->imageBuilder = $imageBuilder; + $this->logger = $logger; + parent::__construct($context); + } + + /** + * Write to the Fera.ai log file + * @param mixed $msg message to log + * @return $this + */ + public function log($msg) + { + $this->logger->info($msg); + return $this; + } + + /** + * Write to the debug output ONLY if the debug mode is enabled + * @param mixed $msg Message to log + * @return $this + */ + public function debug($msg) + { + if ($this->isDebugMode()) { + return $this->log($msg); + } + + return $this; + } + + /** + * @return String Version of the extension (x.x.x) + */ + public function getVersion() + { + return $this->moduleResource->getDbVersion('Fera_Ai'); + } + + /** + * Fera Ai public key either from the store config or the environment files + * @return string + */ + public function getPublicKey() + { + return $this->scopeConfig->getValue( + 'fera_ai/fera_ai_group/public_key', + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Fera Ai secret (private) key, either from the environment fiels or the store config + * @return string + */ + public function getSecretKey() + { + return $this->scopeConfig->getValue( + 'fera_ai/fera_ai_group/secret_key', + ScopeInterface::SCOPE_STORE + ); + } + + public function isEnabled() + { + if (!$this->isConfigured()) { + return false; + } + + return $this->scopeConfig->getValue( + 'fera_ai/general/enabled', + ScopeInterface::SCOPE_STORE + ); + } + + /** + * True if the current Fera Ai configuration is setup to work properly + * @return boolean false if it is not ready for use + */ + public function isConfigured() + { + $publicKey = $this->getPublicKey(); + $secretKey = $this->getSecretKey(); + $apiUrl = $this->getApiUrl(); + $jsUrl = $this->getJsUrl(); + return !empty($publicKey) && !empty($secretKey) && !empty($apiUrl) && !empty($jsUrl); + } + + /** + * The URL path to the API (https). For example: https://api.fera.ai/api/v1 + * @return string + */ + public function getApiUrl() + { + return $this->scopeConfig->getValue( + 'fera_ai/fera_ai_group/api_url', + ScopeInterface::SCOPE_STORE + ); + } + + /** + * The URL to the javascript file on the Fera CDN. For example: https://cdn.fera.ai/js/bananastand.js + * @return string + */ + public function getJsUrl() + { + return $this->scopeConfig->getValue( + 'fera_ai/fera_ai_group/js_url', + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Is debug mode enabled? If so we will output much more extra info to the logs to help developers. + * @return boolean + */ + public function isDebugMode() + { + return $this->scopeConfig->isSetFlag( + 'fera_ai/general/debug_mode', + ScopeInterface::SCOPE_STORE + ); + } + + public function serializeQuoteItems($items) + { + $configurableItems = []; + $itemMap = []; + $childItems = []; + foreach ($items as $cartItem) { + + if ($cartItem->getParentItemId()) { + $childItems[] = $cartItem; + } else { + $itemMap[$cartItem->getId()] = [ + 'product_id' => $cartItem->getProductId(), + 'price' => $cartItem->getPrice(), + 'total' => $cartItem->getRowTotal(), + 'name' => $cartItem->getName() + ]; + if ($cartItem->getProductType() == 'configurable') { + $configurableItems[$cartItem->getId()] = $itemMap[$cartItem->getId()]; + } + } + + } + + foreach ($childItems as $cartItem) { + if ($configurableItems[$cartItem->getParentItemId()]) { + // product is configurable + $itemMap[$cartItem->getParentItemId()]['name'] = $cartItem->getName(); + $itemMap[$cartItem->getParentItemId()]['variant_id'] = $cartItem->getProductId(); + } else { + // product is bundle or something else, just add it as a normal item + + $itemMap[$cartItem->getId()] = [ + 'product_id' => $cartItem->getProductId(), + 'price' => $cartItem->getPrice(), + 'total' => $cartItem->getRowTotal(), + 'name' => $cartItem->getName() + ]; + } + } + + return array_values($itemMap); + } + + /** + * @return string - The contents of the cart as a json string. + */ + public function getCartJson() + { + $quote = $this->checkoutSession->getQuote(); + + $data = [ + 'currency' => $this->storeManager->getStore()->getCurrentCurrency()->getCode(), + 'total' => $quote->getSubtotal(), + 'grand_total' => $quote->getGrandTotal() + ]; + + $data['items'] = $this->serializeQuoteItems($quote->getAllVisibleItems()); + + return $this->jsonEncode($data); + } + + /** + * @return string - JS to trigger debug mode if required. + */ + public function getDebugJs() { + if ($this->isDebugMode()) { + return "window.feraDebugMode = true;"; + } + return ""; + } + + public function jsonEncode($data) + { + return $this->jsonHelper->jsonEncode($data); + } + + public function formatDate($date) + { + return $this->dateTime->create($date)->format(self::FORMAT_DATE); + } + + public function getImage($product, $imageId, $attributes = []) + { + return $this->imageBuilder->setProduct($product) + ->setImageId($imageId) + ->setAttributes($attributes) + ->create(); + } + + public function getProductThumbnailUrl($product) + { + $imageType = 'product_thumbnail_image'; + $image = $this->getImage($product, $imageType); + return $image->getImageUrl(); + } + +} diff --git a/src/app/code/Fera/Ai/Logger/Handler.php b/src/app/code/Fera/Ai/Logger/Handler.php new file mode 100644 index 0000000..b796c66 --- /dev/null +++ b/src/app/code/Fera/Ai/Logger/Handler.php @@ -0,0 +1,30 @@ + + + + + + + +
+ separator-top + + fera_ai + Fera_Ai::fera_config + + + + + Magento\Config\Model\Config\Source\Yesno + + + + Magento\Config\Model\Config\Source\Yesno + + + + + + + + + + You can find your API keys at https://app.fera.ai/store/settings?tab=api + + + + + + + + +
+
+
diff --git a/src/app/code/Fera/Ai/etc/config.xml b/src/app/code/Fera/Ai/etc/config.xml new file mode 100644 index 0000000..173f2f5 --- /dev/null +++ b/src/app/code/Fera/Ai/etc/config.xml @@ -0,0 +1,17 @@ + + + + + + + 1 + 0 + + + https://app.fera.ai/api/v2 + https://cdn.fera.ai/js/fera.js + + + + diff --git a/src/app/code/Fera/Ai/etc/di.xml b/src/app/code/Fera/Ai/etc/di.xml new file mode 100644 index 0000000..55ab5e4 --- /dev/null +++ b/src/app/code/Fera/Ai/etc/di.xml @@ -0,0 +1,20 @@ + + + + + + + + Magento\Framework\Filesystem\Driver\File + + + + + FeraAiLogger + + Fera\Ai\Logger\Handler + + + + diff --git a/src/app/code/Fera/Ai/etc/module.xml b/src/app/code/Fera/Ai/etc/module.xml new file mode 100644 index 0000000..99e8bc7 --- /dev/null +++ b/src/app/code/Fera/Ai/etc/module.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/src/app/code/Fera/Ai/registration.php b/src/app/code/Fera/Ai/registration.php new file mode 100644 index 0000000..8af7bf2 --- /dev/null +++ b/src/app/code/Fera/Ai/registration.php @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/src/app/code/Fera/Ai/view/frontend/layout/checkout_multishipping_success.xml b/src/app/code/Fera/Ai/view/frontend/layout/checkout_multishipping_success.xml new file mode 100644 index 0000000..6c8ec10 --- /dev/null +++ b/src/app/code/Fera/Ai/view/frontend/layout/checkout_multishipping_success.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/app/code/Fera/Ai/view/frontend/layout/checkout_onepage_success.xml b/src/app/code/Fera/Ai/view/frontend/layout/checkout_onepage_success.xml new file mode 100644 index 0000000..6c8ec10 --- /dev/null +++ b/src/app/code/Fera/Ai/view/frontend/layout/checkout_onepage_success.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/app/code/Fera/Ai/view/frontend/layout/default.xml b/src/app/code/Fera/Ai/view/frontend/layout/default.xml new file mode 100644 index 0000000..0f67980 --- /dev/null +++ b/src/app/code/Fera/Ai/view/frontend/layout/default.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/src/app/code/Fera/Ai/view/frontend/templates/footer.phtml b/src/app/code/Fera/Ai/view/frontend/templates/footer.phtml new file mode 100644 index 0000000..a8c8979 --- /dev/null +++ b/src/app/code/Fera/Ai/view/frontend/templates/footer.phtml @@ -0,0 +1,24 @@ +isEnabled()): ?> + + + getChildHtml('fera_ai.footer.product.view'); ?> + getChildHtml('fera_ai.footer.checkout.success'); ?> + + + diff --git a/src/app/code/Fera/Ai/view/frontend/templates/footer/checkout/success.phtml b/src/app/code/Fera/Ai/view/frontend/templates/footer/checkout/success.phtml new file mode 100644 index 0000000..fbaaf42 --- /dev/null +++ b/src/app/code/Fera/Ai/view/frontend/templates/footer/checkout/success.phtml @@ -0,0 +1,6 @@ +isEnabled()): ?> + + diff --git a/src/app/code/Fera/Ai/view/frontend/templates/footer/product/view.phtml b/src/app/code/Fera/Ai/view/frontend/templates/footer/product/view.phtml new file mode 100644 index 0000000..d192479 --- /dev/null +++ b/src/app/code/Fera/Ai/view/frontend/templates/footer/product/view.phtml @@ -0,0 +1,4 @@ + diff --git a/src/app/code/Fera/Ai/view/frontend/templates/footer/product/view/under_add_to_cart.phtml b/src/app/code/Fera/Ai/view/frontend/templates/footer/product/view/under_add_to_cart.phtml new file mode 100644 index 0000000..d2b0b52 --- /dev/null +++ b/src/app/code/Fera/Ai/view/frontend/templates/footer/product/view/under_add_to_cart.phtml @@ -0,0 +1,5 @@ +isEnabled()): ?> +
+