diff --git a/ecommerce-examples/example1/back-end/javascript/constants.js b/ecommerce-examples/example1/back-end/javascript/constants.js new file mode 100644 index 0000000..180432e --- /dev/null +++ b/ecommerce-examples/example1/back-end/javascript/constants.js @@ -0,0 +1,4 @@ +module.exports = { + siteName: 'SITE_ID', + API_KEY: 'API_KEY' +} \ No newline at end of file diff --git a/ecommerce-examples/example1/back-end/javascript/fetch-estimates.js b/ecommerce-examples/example1/back-end/javascript/fetch-estimates.js new file mode 100644 index 0000000..26efbd5 --- /dev/null +++ b/ecommerce-examples/example1/back-end/javascript/fetch-estimates.js @@ -0,0 +1,48 @@ +const fetch = require('node-fetch'); +const { siteName, API_KEY } = require('./constants'); + +let credentialError = null; +if (siteName === 'SITE_ID' || !siteName) { + credentialError = + 'Error: Kindly provide your Chargebee Site name at the Backend'; +} +if (API_KEY === 'API_KEY' || !API_KEY) { + credentialError = + 'Error: Kindly provide your Chargebee Site API key at the Backend'; +} +if (credentialError) { + throw Error(credentialError); +} +module.exports = async (req, res) => { + let data = '', + itemsLength = req.query?.purchase_items?.index?.length || 0; + for (let i = 0; i < itemsLength; i++) { + data = `${data}purchase_items[index][${i}]=${ + i + 1 + }&purchase_items[item_price_id][${i}]=${ + req.query.purchase_items.item_price_id[i] + }&`; + if (req.query.purchase_items.quantity) { + data = `${data}purchase_items[quantity][${i}]=${req.query.purchase_items.quantity[i]}&`; + } + } + if (req.query.customer_id) { + data = data + 'customer_id=' + req.query.customer_id; + } + const response = await fetch( + `https://${siteName}.chargebee.com/api/v2/purchases/estimate?${data}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Authorization: `Basic ${Buffer.from(API_KEY).toString('base64')}` + } + } + ); + const est = await response.json(); + if (est?.estimate?.invoice_estimates) { + res.status(200).json(est.estimate.invoice_estimates[0]); + } else { + res.status(400).json(est); + } +}; diff --git a/ecommerce-examples/example1/back-end/javascript/fetch-items.js b/ecommerce-examples/example1/back-end/javascript/fetch-items.js index 89a292f..691c370 100644 --- a/ecommerce-examples/example1/back-end/javascript/fetch-items.js +++ b/ecommerce-examples/example1/back-end/javascript/fetch-items.js @@ -12,7 +12,7 @@ const fetchItems = (req, res) => { //handle error console.log(error); } else { - var item; + var item = {}; if (result.list.length) { item = result.list[0].item; } diff --git a/ecommerce-examples/example1/back-end/javascript/fetch-product.js b/ecommerce-examples/example1/back-end/javascript/fetch-product.js new file mode 100644 index 0000000..81a3808 --- /dev/null +++ b/ecommerce-examples/example1/back-end/javascript/fetch-product.js @@ -0,0 +1,31 @@ +const fetch = require('node-fetch'); +const { siteName, API_KEY } = require('./constants'); + +let credentialError = null; +if (siteName === 'SITE_ID' || !siteName) { + credentialError = + 'Error: Kindly provide your Chargebee Site name at the Backend'; +} +if (API_KEY === 'API_KEY' || !API_KEY) { + credentialError = + 'Error: Kindly provide your Chargebee Site API key at the Backend'; +} +if (credentialError) { + throw Error(credentialError); +} +module.exports = async (req, res) => { + try { + const response = await fetch( + `https://${siteName}.chargebee.com/api/v2/products/${req.query.product_id}`, + { + headers: { + Authorization: `Basic ${Buffer.from(API_KEY).toString('base64')}` + } + } + ); + const productWrapper = await response.json(); + res.status(200).json(productWrapper.product); + } catch (err) { + console.log(err); + } +}; diff --git a/ecommerce-examples/example1/back-end/javascript/fetch-variants.js b/ecommerce-examples/example1/back-end/javascript/fetch-variants.js index 8d88760..19ffd92 100644 --- a/ecommerce-examples/example1/back-end/javascript/fetch-variants.js +++ b/ecommerce-examples/example1/back-end/javascript/fetch-variants.js @@ -1,6 +1,5 @@ const fetch = require('node-fetch'); -const siteName = 'SITE_ID'; -const API_KEY = 'API_KEY'; +const { siteName, API_KEY } = require('./constants'); let credentialError = null; if (siteName === 'SITE_ID' || !siteName) { @@ -15,19 +14,23 @@ if (credentialError) { throw Error(credentialError); } module.exports = async (req, res) => { - let variants = []; - const response = await fetch( - `https://${siteName}.chargebee.com/api/v2/products/${req.query.product_id}/variants`, - { - headers: { - Authorization: `Basic ${Buffer.from(API_KEY).toString('base64')}` + try { + let variants = []; + const response = await fetch( + `https://${siteName}.chargebee.com/api/v2/products/${req.query.product_id}/variants`, + { + headers: { + Authorization: `Basic ${Buffer.from(API_KEY).toString('base64')}` + } } + ); + const varJson = await response.json(); + for (let i = 0; i < varJson.list?.length; i++) { + const variant = varJson.list[i].variant; + variants.push(variant); } - ); - const varJson = await response.json(); - for (let i = 0; i < varJson.list.length; i++) { - const variant = varJson.list[i].variant; - variants.push(variant); + res.status(200).json({ list: variants }); + } catch (err) { + console.log(err); } - res.status(200).json({ list: variants }); }; diff --git a/ecommerce-examples/example1/back-end/javascript/index.js b/ecommerce-examples/example1/back-end/javascript/index.js index 7c6921c..9fd0559 100644 --- a/ecommerce-examples/example1/back-end/javascript/index.js +++ b/ecommerce-examples/example1/back-end/javascript/index.js @@ -4,16 +4,18 @@ const path = require('path'); const fetchItems = require('./fetch-items'); const fetchItemPrices = require('./fetch-item-prices'); const fetchVariants = require('./fetch-variants'); +const fetchProduct = require('./fetch-product'); const checkoutNew = require('./new-checkout'); +const estimates = require('./fetch-estimates'); +const { siteName, API_KEY } = require('./constants'); // CORS is enabled only for demo. Please dont use this in production unless you know about CORS const cors = require('cors'); -const siteName = 'SITE_ID'; -const API_KEY = 'API_KEY'; let credentialError = null; if (siteName === 'SITE_ID' || !siteName) { - credentialError = 'Error: Kindly provide your Chargebee Site name at the Backend' + credentialError = + 'Error: Kindly provide your Chargebee Site name at the Backend'; } if (API_KEY === 'API_KEY' || !API_KEY) { credentialError = @@ -28,7 +30,8 @@ chargebee.configure({ }); const app = express(); -app.use(express.urlencoded()); +app.use(express.json()); +app.use(express.urlencoded({extended: true})); app.use(cors()); // Configure your static file paths here. Images, CSS and JS files should be inside this path @@ -54,16 +57,37 @@ app.get('/api/variants', async (req, res) => { await fetchVariants(req, res); }); +/* + Fetch Product API + request params - Product ID +*/ +app.get('/api/product', async (req, res) => { + await fetchProduct(req, res); +}); + /* Fetch Checkout Link request params - Item Price ID, Customer ID (optional) */ app.post('/api/generate_checkout_new_url', checkoutNew); +/* + Fetch Estimates for the Cart items +*/ +app.post('/api/calculate_estimates', async (req, res) => { + await estimates(req, res); +}); + // Configure the path of your HTML file to be loaded app.get('/', (req, res) => { res.sendFile( - path.join(__dirname, '../../front-end/javascript/cb-widget.html') + path.join(__dirname, '../../front-end/javascript/widget/cb-widget.html') + ); +}); + +app.get('/cart', (req, res) => { + res.sendFile( + path.join(__dirname, '../../front-end/javascript/cart/cb-cart.html') ); }); diff --git a/ecommerce-examples/example1/front-end/javascript/cart/cb-cart.css b/ecommerce-examples/example1/front-end/javascript/cart/cb-cart.css new file mode 100644 index 0000000..5c6ff33 --- /dev/null +++ b/ecommerce-examples/example1/front-end/javascript/cart/cb-cart.css @@ -0,0 +1,169 @@ +.cb-cart-container { + width: 25%; + height: 100vh; + position: fixed; + top: 0; + right: 0; + background: white; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + z-index: 1; + transition: all 0.3s ease; + transform: translateX(100%); +} + +.cb-cart-container.open { + transform: translateX(0%); +} + +.cb-cart-container.open .cb-cart-icon { + display: none; +} + +.cb-cart-container.close .cb-close-icon { + display: none; +} + +.cb-cart-button { + position: absolute; + transform: translateX(-100%); + padding: 10px 10px 10px 15px; + background: white; + border-radius: 20px 0px 0px 20px; + top: 20px; + box-shadow: -10px 6px 13px #f4f4f4; + cursor: pointer; +} + +.cb-cart-quantity { + position: absolute; + top: -12px; + left: -12px; + background: #022938; + padding: 5px 10px; + border-radius: 50%; + color: white; + font-family: var(--cb-font-family); +} + +.cb-cart-empty { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + flex-direction: column; +} + +.cb-clear-cart { + display: flex; + justify-content: flex-end; + margin-bottom: 30px; +} + +.cb-clear-cart .cb-cta-secondary { + margin-right: 20px; +} + +.cb-cart-items-wrapper { + padding: 0 30px; + overflow: auto; + max-height: 70vh; +} + +.cb-cart-item-remove { + font-size: 12px; + cursor: pointer; + color: #c81f1f; + font-weight: 600; + display: inline-block; +} + +.cb-delivery-info { + font-size: 12px; +} + +.cb-cart-total-price { + padding: 30px 50px; + display: flex; + justify-content: space-between; + font-size: 24px; + font-weight: 600; + position: absolute; + bottom: 0; + left: 0; + width: 20vw; + background: white; + box-shadow: rgba(0, 0, 0, 0.35) 15px 5px 15px; +} + +.cb-cart-total-price div:first-child { + margin-right: 50px; +} + +.cb-customer-info { + margin: auto; + width: 70%; +} + +.cb-customer-info input, +.cb-customer-info select { + height: 32px; + width: 100%; + margin: 10px 0; + padding: 0 10px; + border-radius: 5px; + border: 1px solid #9797a5; +} + +.cb-item-image img { + width: 125px; + border-radius: 5px; +} + +.cb-cart-item-container { + display: flex; + padding: 15px 0; + border-top: 1px solid #9797a5; + align-items: center; +} + +.cb-item-detail { + padding: 0 15px; +} + +.cb-productInfo, +.cb-unitPrice, +.cb-cart-item-quantity { + margin-bottom: 10px; +} + +.cb-cart-item-quantity { + display: flex; + justify-content: space-between; + align-items: center; +} + +.cb-cart-item-container .cb-quantity-wrapper { + border: none; +} + +.cb-cart-item-container .cb-quantity-wrapper button { + border-radius: 50%; + border: 1px solid #d7d5dd; + padding: 5px 10px; + background: #d7d5dd; + cursor: pointer; +} + +.cb-continue-shopping { + padding: 10px 20px; + background-color: var(--cb-cta-bg); + color: var(--cb-cta-text); + border-radius: var(--cb-border-radius); + text-decoration: none; + display: block; + text-align: center; + border: none; + width: 80%; + margin: 30px auto; + cursor: pointer; +} \ No newline at end of file diff --git a/ecommerce-examples/example1/front-end/javascript/cart/cb-cart.html b/ecommerce-examples/example1/front-end/javascript/cart/cb-cart.html new file mode 100644 index 0000000..1feed16 --- /dev/null +++ b/ecommerce-examples/example1/front-end/javascript/cart/cb-cart.html @@ -0,0 +1,62 @@ + +
+
+ + + + + 0 +
+
+ + + + + + + + + + +
+
+
+ +

Your cart is empty!

+
+
+
+ +
+
+ +
+
Total
+
0.00
+
+
diff --git a/ecommerce-examples/example1/front-end/javascript/cart/cb-cart.js b/ecommerce-examples/example1/front-end/javascript/cart/cb-cart.js new file mode 100644 index 0000000..772d5fa --- /dev/null +++ b/ecommerce-examples/example1/front-end/javascript/cart/cb-cart.js @@ -0,0 +1,258 @@ +var cbInstance = Chargebee.init({ + site: 'pc-pim-test', // your test site + isItemsModel: true +}); + +const CbCart = { + inited: false, + options: {}, + estimates: null, + init: async function (options = {}) { + this.options = { + ...this.options, + ...options + }; + await this.embedCart(); + document + .querySelector('.cb-cart-button') + .addEventListener('click', this.toggleCart); + this.initializeLocalStorage(); + }, + embedCart: async function () { + if (this.inited) return; + const fetchTemplate = await fetch('/cart'); + const cart = await fetchTemplate.text(); + document.querySelector('.cb-cart-container').innerHTML = cart; + this.inited = true; + }, + initializeLocalStorage: function () { + let localCart = localStorage.getItem('CbCart'); + if (localCart) { + const { lastUpdated } = JSON.parse(localCart); + const timeDiff = (Date.now() - lastUpdated) / (1000 * 60 * 60 * 24); + if (parseInt(timeDiff) > 30) { + localCart = null; + } + } + if (!localStorage.getItem('CbCart')) { + localStorage.setItem( + 'CbCart', + JSON.stringify({ + ...(localCart || { + ...cbInstance.getCart() + }), + lastUpdated: Date.now() + }) + ); + } + const cartProducts = JSON.parse(localCart)?.products || []; + const cartInstance = cbInstance.getCart(); + cartInstance.products = cartProducts; + for (let i = 0; i < cartProducts.length; i++) { + this.renderCartItem(cartProducts[i]); + } + }, + updateLocalStorage: function () { + localStorage.setItem( + 'CbCart', + JSON.stringify({ + ...cbInstance.getCart(), + lastUpdated: Date.now() + }) + ); + }, + addProductToCart: function (productOptions) { + const cart = cbInstance.getCart(); + const existingProduct = this.findProduct(productOptions.itemPriceId); + if (existingProduct) { + if (existingProduct.planQuantity + productOptions.quantity < 1) return; + existingProduct.planQuantity += productOptions.quantity; + existingProduct.data = { + ...existingProduct.data, + quantity: existingProduct.planQuantity + }; + this.updateCartRow(existingProduct); + return; + } + const product = cbInstance.initializeProduct( + productOptions.itemId, + productOptions.quantity, + true + ); + product.addItem( + product.createItem( + productOptions.itemPriceId, + productOptions.quantity, + productOptions.type + ) + ); + this.addCustomData(product, productOptions); + cart.products.push(product); + this.renderCartItem(product); + }, + addCustomData: function (product, productParams) { + product.data = { + productName: productParams.productInfo.name, + image: productParams.productInfo.image, + variantName: productParams.productInfo.variantName, + deliveryInfo: productParams.productInfo.deliveryInfo, + quantity: product.planQuantity, + unitPrice: productParams.productInfo.price, + currencyCode: this.options.currency + }; + }, + renderCartItem: function (product) { + const template = (data) => { + return ` +
+ +
+
+
+ ${data.productName} - ${data.variantName} +
+ ${data.deliveryInfo} +
+
+
+ ${data.unitPrice} ${data.currency} +
+
+ +
+ + ${data.quantity} + +
+
+
+ Remove +
+
+ `; + }; + const wrapper = document.querySelector('.cb-cart-items-wrapper'); + const row = document.createElement('div'); + row.id = `price-${product.items[0].item_price_id}`; + row.className = 'cb-cart-item-container'; + row.innerHTML = template({ + id: product.items[0].item_price_id, + ...product.data, + currency: this.options.currency + }); + wrapper.appendChild(row); + this.updateCartQuantity(); + this.calculateEstimate(); + if (!document.querySelector('.cb-cart-empty.cb-hide')) { + document.querySelector('.cb-cart-empty').className = + 'cb-cart-empty cb-hide'; + document.querySelector('.cb-cart-hasitem').className = 'cb-cart-hasitem'; + } + }, + updateCartQuantity: function () { + const cart = cbInstance.getCart(); + const countContainer = document.querySelector('.cb-cart-quantity'); + countContainer.innerText = cart.products.length; + }, + updateCartRow: function (product) { + const priceId = product.items[0].item_price_id; + const row = document.querySelector(`#price-${priceId}`); + row.querySelector('.cb-cart-item-quantity-input').innerText = + product.planQuantity; + this.calculateEstimate(); + }, + deleteCartItem: function (itemPriceId) { + const cart = cbInstance.getCart(); + const productIndex = cart.products.findIndex((item) => { + return item.items[0].item_price_id === itemPriceId; + }); + document.querySelector(`#price-${itemPriceId}`)?.remove(); + cart.products.splice(productIndex, 1); + this.updateCartQuantity(); + if (!cart.products.length) { + this.clearCart(); + } else { + this.calculateEstimate(); + } + }, + calculateEstimate: async function () { + let query = ''; + const cart = cbInstance.getCart(); + cart.products.forEach((product, index) => { + query = `${query}purchase_items[index][${index}]=${ + index + 1 + }&purchase_items[item_price_id][${index}]=${ + product.items[0].item_price_id + }&`; + query = `${query}purchase_items[quantity][${index}]=${product.planQuantity}&`; + }); + document.querySelector('#cb-cart-total-display').innerText = + 'calculating...'; + try { + const res = await fetch(`/api/calculate_estimates?${query}`, { + method: 'POST', + headers: { + 'Content-type': 'application/json; charset=UTF-8' + } + }); + const estimates = await res.json(); + this.estimates = estimates; + } catch (err) { + console.error(err); + } + this.updateCartPrice(); + this.updateLocalStorage(); + }, + updateCartPrice: function () { + if (this.estimates.total) { + document.querySelector('#cb-cart-total-display').innerText = `${( + this.estimates.total / 100 + ).toFixed(2)} ${this.options.currency}`; + } + }, + findProduct: function (itemPriceId) { + const cart = cbInstance.getCart(); + const productIndex = cart.products.findIndex((item) => { + return item.items[0].item_price_id === itemPriceId; + }); + return productIndex !== -1 ? cart.products[productIndex] : null; + }, + modifyQuantity: function (itemPriceId, qty) { + this.addProductToCart({ + itemPriceId, + quantity: qty + }); + }, + clearCart: function () { + const cart = cbInstance.getCart(); + cart.products = []; + document.querySelector('.cb-cart-empty').className = 'cb-cart-empty'; + document.querySelector('.cb-cart-hasitem').className = + 'cb-cart-hasitem cb-hide'; + document.querySelector('.cb-cart-items-wrapper').innerHTML = ''; + this.updateCartQuantity(); + this.updateLocalStorage(); + }, + toggleCart: function () { + if (document.querySelector('.cb-cart-container.open')) { + document.querySelector('.cb-cart-container').className = + 'cb-cart-container close'; + } else { + document.querySelector('.cb-cart-container').className = + 'cb-cart-container open'; + } + } +}; + +CbCart.init({ + currency: 'USD' +}); diff --git a/ecommerce-examples/example1/front-end/javascript/cb-widget.html b/ecommerce-examples/example1/front-end/javascript/cb-widget.html deleted file mode 100644 index ef4f1ce..0000000 --- a/ecommerce-examples/example1/front-end/javascript/cb-widget.html +++ /dev/null @@ -1,185 +0,0 @@ - - - - PDP Widget - - - - -
- -
Loading...
- - - - -
- - - - diff --git a/ecommerce-examples/example1/front-end/javascript/constants/countries.js b/ecommerce-examples/example1/front-end/javascript/constants/countries.js new file mode 100644 index 0000000..4fc7e44 --- /dev/null +++ b/ecommerce-examples/example1/front-end/javascript/constants/countries.js @@ -0,0 +1,251 @@ +const CbCountries = [ + { Code: 'AF', Name: 'Afghanistan' }, + { Code: 'AX', Name: '\u00c5land Islands' }, + { Code: 'AL', Name: 'Albania' }, + { Code: 'DZ', Name: 'Algeria' }, + { Code: 'AS', Name: 'American Samoa' }, + { Code: 'AD', Name: 'Andorra' }, + { Code: 'AO', Name: 'Angola' }, + { Code: 'AI', Name: 'Anguilla' }, + { Code: 'AQ', Name: 'Antarctica' }, + { Code: 'AG', Name: 'Antigua and Barbuda' }, + { Code: 'AR', Name: 'Argentina' }, + { Code: 'AM', Name: 'Armenia' }, + { Code: 'AW', Name: 'Aruba' }, + { Code: 'AU', Name: 'Australia' }, + { Code: 'AT', Name: 'Austria' }, + { Code: 'AZ', Name: 'Azerbaijan' }, + { Code: 'BS', Name: 'Bahamas' }, + { Code: 'BH', Name: 'Bahrain' }, + { Code: 'BD', Name: 'Bangladesh' }, + { Code: 'BB', Name: 'Barbados' }, + { Code: 'BY', Name: 'Belarus' }, + { Code: 'BE', Name: 'Belgium' }, + { Code: 'BZ', Name: 'Belize' }, + { Code: 'BJ', Name: 'Benin' }, + { Code: 'BM', Name: 'Bermuda' }, + { Code: 'BT', Name: 'Bhutan' }, + { Code: 'BO', Name: 'Bolivia, Plurinational State of' }, + { Code: 'BQ', Name: 'Bonaire, Sint Eustatius and Saba' }, + { Code: 'BA', Name: 'Bosnia and Herzegovina' }, + { Code: 'BW', Name: 'Botswana' }, + { Code: 'BV', Name: 'Bouvet Island' }, + { Code: 'BR', Name: 'Brazil' }, + { Code: 'IO', Name: 'British Indian Ocean Territory' }, + { Code: 'BN', Name: 'Brunei Darussalam' }, + { Code: 'BG', Name: 'Bulgaria' }, + { Code: 'BF', Name: 'Burkina Faso' }, + { Code: 'BI', Name: 'Burundi' }, + { Code: 'KH', Name: 'Cambodia' }, + { Code: 'CM', Name: 'Cameroon' }, + { Code: 'CA', Name: 'Canada' }, + { Code: 'CV', Name: 'Cape Verde' }, + { Code: 'KY', Name: 'Cayman Islands' }, + { Code: 'CF', Name: 'Central African Republic' }, + { Code: 'TD', Name: 'Chad' }, + { Code: 'CL', Name: 'Chile' }, + { Code: 'CN', Name: 'China' }, + { Code: 'CX', Name: 'Christmas Island' }, + { Code: 'CC', Name: 'Cocos (Keeling) Islands' }, + { Code: 'CO', Name: 'Colombia' }, + { Code: 'KM', Name: 'Comoros' }, + { Code: 'CG', Name: 'Congo' }, + { Code: 'CD', Name: 'Congo, the Democratic Republic of the' }, + { Code: 'CK', Name: 'Cook Islands' }, + { Code: 'CR', Name: 'Costa Rica' }, + { Code: 'CI', Name: "C\u00f4te d'Ivoire" }, + { Code: 'HR', Name: 'Croatia' }, + { Code: 'CU', Name: 'Cuba' }, + { Code: 'CW', Name: 'Cura\u00e7ao' }, + { Code: 'CY', Name: 'Cyprus' }, + { Code: 'CZ', Name: 'Czech Republic' }, + { Code: 'DK', Name: 'Denmark' }, + { Code: 'DJ', Name: 'Djibouti' }, + { Code: 'DM', Name: 'Dominica' }, + { Code: 'DO', Name: 'Dominican Republic' }, + { Code: 'EC', Name: 'Ecuador' }, + { Code: 'EG', Name: 'Egypt' }, + { Code: 'SV', Name: 'El Salvador' }, + { Code: 'GQ', Name: 'Equatorial Guinea' }, + { Code: 'ER', Name: 'Eritrea' }, + { Code: 'EE', Name: 'Estonia' }, + { Code: 'ET', Name: 'Ethiopia' }, + { Code: 'FK', Name: 'Falkland Islands (Malvinas)' }, + { Code: 'FO', Name: 'Faroe Islands' }, + { Code: 'FJ', Name: 'Fiji' }, + { Code: 'FI', Name: 'Finland' }, + { Code: 'FR', Name: 'France' }, + { Code: 'GF', Name: 'French Guiana' }, + { Code: 'PF', Name: 'French Polynesia' }, + { Code: 'TF', Name: 'French Southern Territories' }, + { Code: 'GA', Name: 'Gabon' }, + { Code: 'GM', Name: 'Gambia' }, + { Code: 'GE', Name: 'Georgia' }, + { Code: 'DE', Name: 'Germany' }, + { Code: 'GH', Name: 'Ghana' }, + { Code: 'GI', Name: 'Gibraltar' }, + { Code: 'GR', Name: 'Greece' }, + { Code: 'GL', Name: 'Greenland' }, + { Code: 'GD', Name: 'Grenada' }, + { Code: 'GP', Name: 'Guadeloupe' }, + { Code: 'GU', Name: 'Guam' }, + { Code: 'GT', Name: 'Guatemala' }, + { Code: 'GG', Name: 'Guernsey' }, + { Code: 'GN', Name: 'Guinea' }, + { Code: 'GW', Name: 'Guinea-Bissau' }, + { Code: 'GY', Name: 'Guyana' }, + { Code: 'HT', Name: 'Haiti' }, + { Code: 'HM', Name: 'Heard Island and McDonald Islands' }, + { Code: 'VA', Name: 'Holy See (Vatican City State)' }, + { Code: 'HN', Name: 'Honduras' }, + { Code: 'HK', Name: 'Hong Kong' }, + { Code: 'HU', Name: 'Hungary' }, + { Code: 'IS', Name: 'Iceland' }, + { Code: 'IN', Name: 'India' }, + { Code: 'ID', Name: 'Indonesia' }, + { Code: 'IR', Name: 'Iran, Islamic Republic of' }, + { Code: 'IQ', Name: 'Iraq' }, + { Code: 'IE', Name: 'Ireland' }, + { Code: 'IM', Name: 'Isle of Man' }, + { Code: 'IL', Name: 'Israel' }, + { Code: 'IT', Name: 'Italy' }, + { Code: 'JM', Name: 'Jamaica' }, + { Code: 'JP', Name: 'Japan' }, + { Code: 'JE', Name: 'Jersey' }, + { Code: 'JO', Name: 'Jordan' }, + { Code: 'KZ', Name: 'Kazakhstan' }, + { Code: 'KE', Name: 'Kenya' }, + { Code: 'KI', Name: 'Kiribati' }, + { Code: 'KP', Name: "Korea, Democratic People's Republic of" }, + { Code: 'KR', Name: 'Korea, Republic of' }, + { Code: 'KW', Name: 'Kuwait' }, + { Code: 'KG', Name: 'Kyrgyzstan' }, + { Code: 'LA', Name: "Lao People's Democratic Republic" }, + { Code: 'LV', Name: 'Latvia' }, + { Code: 'LB', Name: 'Lebanon' }, + { Code: 'LS', Name: 'Lesotho' }, + { Code: 'LR', Name: 'Liberia' }, + { Code: 'LY', Name: 'Libya' }, + { Code: 'LI', Name: 'Liechtenstein' }, + { Code: 'LT', Name: 'Lithuania' }, + { Code: 'LU', Name: 'Luxembourg' }, + { Code: 'MO', Name: 'Macao' }, + { Code: 'MK', Name: 'Macedonia, the Former Yugoslav Republic of' }, + { Code: 'MG', Name: 'Madagascar' }, + { Code: 'MW', Name: 'Malawi' }, + { Code: 'MY', Name: 'Malaysia' }, + { Code: 'MV', Name: 'Maldives' }, + { Code: 'ML', Name: 'Mali' }, + { Code: 'MT', Name: 'Malta' }, + { Code: 'MH', Name: 'Marshall Islands' }, + { Code: 'MQ', Name: 'Martinique' }, + { Code: 'MR', Name: 'Mauritania' }, + { Code: 'MU', Name: 'Mauritius' }, + { Code: 'YT', Name: 'Mayotte' }, + { Code: 'MX', Name: 'Mexico' }, + { Code: 'FM', Name: 'Micronesia, Federated States of' }, + { Code: 'MD', Name: 'Moldova, Republic of' }, + { Code: 'MC', Name: 'Monaco' }, + { Code: 'MN', Name: 'Mongolia' }, + { Code: 'ME', Name: 'Montenegro' }, + { Code: 'MS', Name: 'Montserrat' }, + { Code: 'MA', Name: 'Morocco' }, + { Code: 'MZ', Name: 'Mozambique' }, + { Code: 'MM', Name: 'Myanmar' }, + { Code: 'NA', Name: 'Namibia' }, + { Code: 'NR', Name: 'Nauru' }, + { Code: 'NP', Name: 'Nepal' }, + { Code: 'NL', Name: 'Netherlands' }, + { Code: 'NC', Name: 'New Caledonia' }, + { Code: 'NZ', Name: 'New Zealand' }, + { Code: 'NI', Name: 'Nicaragua' }, + { Code: 'NE', Name: 'Niger' }, + { Code: 'NG', Name: 'Nigeria' }, + { Code: 'NU', Name: 'Niue' }, + { Code: 'NF', Name: 'Norfolk Island' }, + { Code: 'MP', Name: 'Northern Mariana Islands' }, + { Code: 'NO', Name: 'Norway' }, + { Code: 'OM', Name: 'Oman' }, + { Code: 'PK', Name: 'Pakistan' }, + { Code: 'PW', Name: 'Palau' }, + { Code: 'PS', Name: 'Palestine, State of' }, + { Code: 'PA', Name: 'Panama' }, + { Code: 'PG', Name: 'Papua New Guinea' }, + { Code: 'PY', Name: 'Paraguay' }, + { Code: 'PE', Name: 'Peru' }, + { Code: 'PH', Name: 'Philippines' }, + { Code: 'PN', Name: 'Pitcairn' }, + { Code: 'PL', Name: 'Poland' }, + { Code: 'PT', Name: 'Portugal' }, + { Code: 'PR', Name: 'Puerto Rico' }, + { Code: 'QA', Name: 'Qatar' }, + { Code: 'RE', Name: 'R\u00e9union' }, + { Code: 'RO', Name: 'Romania' }, + { Code: 'RU', Name: 'Russian Federation' }, + { Code: 'RW', Name: 'Rwanda' }, + { Code: 'BL', Name: 'Saint Barth\u00e9lemy' }, + { Code: 'SH', Name: 'Saint Helena, Ascension and Tristan da Cunha' }, + { Code: 'KN', Name: 'Saint Kitts and Nevis' }, + { Code: 'LC', Name: 'Saint Lucia' }, + { Code: 'MF', Name: 'Saint Martin (French part)' }, + { Code: 'PM', Name: 'Saint Pierre and Miquelon' }, + { Code: 'VC', Name: 'Saint Vincent and the Grenadines' }, + { Code: 'WS', Name: 'Samoa' }, + { Code: 'SM', Name: 'San Marino' }, + { Code: 'ST', Name: 'Sao Tome and Principe' }, + { Code: 'SA', Name: 'Saudi Arabia' }, + { Code: 'SN', Name: 'Senegal' }, + { Code: 'RS', Name: 'Serbia' }, + { Code: 'SC', Name: 'Seychelles' }, + { Code: 'SL', Name: 'Sierra Leone' }, + { Code: 'SG', Name: 'Singapore' }, + { Code: 'SX', Name: 'Sint Maarten (Dutch part)' }, + { Code: 'SK', Name: 'Slovakia' }, + { Code: 'SI', Name: 'Slovenia' }, + { Code: 'SB', Name: 'Solomon Islands' }, + { Code: 'SO', Name: 'Somalia' }, + { Code: 'ZA', Name: 'South Africa' }, + { Code: 'GS', Name: 'South Georgia and the South Sandwich Islands' }, + { Code: 'SS', Name: 'South Sudan' }, + { Code: 'ES', Name: 'Spain' }, + { Code: 'LK', Name: 'Sri Lanka' }, + { Code: 'SD', Name: 'Sudan' }, + { Code: 'SR', Name: 'Suriname' }, + { Code: 'SJ', Name: 'Svalbard and Jan Mayen' }, + { Code: 'SZ', Name: 'Swaziland' }, + { Code: 'SE', Name: 'Sweden' }, + { Code: 'CH', Name: 'Switzerland' }, + { Code: 'SY', Name: 'Syrian Arab Republic' }, + { Code: 'TW', Name: 'Taiwan, Province of China' }, + { Code: 'TJ', Name: 'Tajikistan' }, + { Code: 'TZ', Name: 'Tanzania, United Republic of' }, + { Code: 'TH', Name: 'Thailand' }, + { Code: 'TL', Name: 'Timor-Leste' }, + { Code: 'TG', Name: 'Togo' }, + { Code: 'TK', Name: 'Tokelau' }, + { Code: 'TO', Name: 'Tonga' }, + { Code: 'TT', Name: 'Trinidad and Tobago' }, + { Code: 'TN', Name: 'Tunisia' }, + { Code: 'TR', Name: 'Turkey' }, + { Code: 'TM', Name: 'Turkmenistan' }, + { Code: 'TC', Name: 'Turks and Caicos Islands' }, + { Code: 'TV', Name: 'Tuvalu' }, + { Code: 'UG', Name: 'Uganda' }, + { Code: 'UA', Name: 'Ukraine' }, + { Code: 'AE', Name: 'United Arab Emirates' }, + { Code: 'GB', Name: 'United Kingdom' }, + { Code: 'US', Name: 'United States' }, + { Code: 'UM', Name: 'United States Minor Outlying Islands' }, + { Code: 'UY', Name: 'Uruguay' }, + { Code: 'UZ', Name: 'Uzbekistan' }, + { Code: 'VU', Name: 'Vanuatu' }, + { Code: 'VE', Name: 'Venezuela, Bolivarian Republic of' }, + { Code: 'VN', Name: 'Viet Nam' }, + { Code: 'VG', Name: 'Virgin Islands, British' }, + { Code: 'VI', Name: 'Virgin Islands, U.S.' }, + { Code: 'WF', Name: 'Wallis and Futuna' }, + { Code: 'EH', Name: 'Western Sahara' }, + { Code: 'YE', Name: 'Yemen' }, + { Code: 'ZM', Name: 'Zambia' }, + { Code: 'ZW', Name: 'Zimbabwe' } +]; diff --git a/ecommerce-examples/example1/front-end/javascript/images/empty-cart.png b/ecommerce-examples/example1/front-end/javascript/images/empty-cart.png new file mode 100644 index 0000000..5664add Binary files /dev/null and b/ecommerce-examples/example1/front-end/javascript/images/empty-cart.png differ diff --git a/ecommerce-examples/example1/front-end/javascript/widget/cb-widget.css b/ecommerce-examples/example1/front-end/javascript/widget/cb-widget.css new file mode 100644 index 0000000..ebdded0 --- /dev/null +++ b/ecommerce-examples/example1/front-end/javascript/widget/cb-widget.css @@ -0,0 +1,157 @@ +/* + CSS Variables - Configure styling by modifying these variables +*/ +body { + --cb-font-family: system-ui, -apple-system, BlinkMacSystemFont, + 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', + 'Helvetica Neue', sans-serif; + --cb-font-size: 14px; + --cb-font-color: #012a38; + --cb-border-radius: 4px; + --cb-box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px; + --cb-widget-bg: #fff; + --cb-cta-bg: #012a38; + --cb-cta-text: #fff; + --cb-variant-bg: #fff; + --cb-variant-active-bg: #137cb6; + --cb-variant-border: 1px solid #012a38; +} + +body { + min-height: 100vh; + font-family: var(--cb-font-family); +} + +body .cb-hide { + display: none; +} + +.cb-loading-container { + text-align: center; + margin-top: 180px; +} + +.cb-button-disabled { + pointer-events: none; + cursor: not-allowed; + opacity: 0.5; +} + +.cb-widget-container { + position: fixed; + min-height: 400px; + width: 400px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-width: 400px; + padding: 30px 20px; + border: 1px solid #ccc; + border-radius: var(--cb-border-radius); + font-family: var(--cb-font-family); + font-size: var(--cb-font-size); + color: var(--cb-font-color); + box-shadow: var(--cb-box-shadow); + background-color: var(--cb-widget-bg); +} + +.cb-widget-title { + margin-top: 0; +} + +.cb-variant-select-wrapper, +.cb-frequency-select, +.cb-frequency-select select, +.cb-quantity { + margin-bottom: 10px; +} + +.cb-quantity { + padding-top: 20px; +} + +.cb-quantity label { + margin-right: 20px; +} + +.cb-quantity button { + border: none; + padding: 5px 10px; + cursor: pointer; +} + +.cb-quantity-wrapper { + display: inline-block; + border: 1px solid gray; +} + +.cb-quantity-wrapper span { + margin: 0 15px; +} + +.cb-frequency-select select, +.cb-variant-select { + display: block; + margin-top: 10px; + width: 100%; + height: 36px; + padding: 5px; + border-radius: var(--cb-border-radius); +} + +.cb-cta-button { + padding: 10px 20px; + background-color: var(--cb-cta-bg); + color: var(--cb-cta-text); + border-radius: var(--cb-border-radius); + text-decoration: none; + display: block; + text-align: center; + margin-top: 10px; +} + +.cb-cta-secondary { + padding: 10px 20px; + background-color: #c81f1e; + color: var(--cb-cta-text); + border-radius: var(--cb-border-radius); + text-decoration: none; + display: block; + text-align: center; + margin-top: 10px; + border: none; + cursor: pointer; +} + +.cb-variant-selector { + padding: 10px 0; + display: flex; + flex-wrap: wrap; + justify-content: space-evenly; +} + +.cb-variant-selector button { + margin-top: 10px; + padding: 10px 16px; + border-radius: var(--cb-border-radius); + border: var(--cb-variant-border); + background: var(--cb-variant-bg); + cursor: pointer; +} + +.cb-variant-selector button.active { + background: var(--cb-variant-active-bg); + color: #fff; + border: 0; +} + +.cb-cta-container { + margin-top: 30px; +} + +.cb-add-to-cart { + background-color: #e5f3fa; + border-color: #8ccce6; + color: #1d485f; + border: 1px solid #0229383d; +} \ No newline at end of file diff --git a/ecommerce-examples/example1/front-end/javascript/widget/cb-widget.html b/ecommerce-examples/example1/front-end/javascript/widget/cb-widget.html new file mode 100644 index 0000000..ea15f22 --- /dev/null +++ b/ecommerce-examples/example1/front-end/javascript/widget/cb-widget.html @@ -0,0 +1,68 @@ + + + + PDP Widget + + + + +
+ +
Loading...
+ + + + +
+ +
+ + + + + diff --git a/ecommerce-examples/example1/front-end/javascript/cb-widget.js b/ecommerce-examples/example1/front-end/javascript/widget/cb-widget.js similarity index 76% rename from ecommerce-examples/example1/front-end/javascript/cb-widget.js rename to ecommerce-examples/example1/front-end/javascript/widget/cb-widget.js index 5e3ff83..8eb06e6 100644 --- a/ecommerce-examples/example1/front-end/javascript/cb-widget.js +++ b/ecommerce-examples/example1/front-end/javascript/widget/cb-widget.js @@ -9,6 +9,7 @@ const CbWidget = { showAddToCart: true, // Display Add to Cart in your widget showSubscribeNow: true // Display Subscribe now in your widget }, + productInfo: {}, variants: [], quantity: 1, prices: { @@ -54,14 +55,32 @@ const CbWidget = { } // Add event handlers for '-' and '+' buttons of Quantity selectors document - .querySelector('.cb-decrement-btn') + .querySelector('.cb-quantity .cb-decrement-btn') .addEventListener('click', () => this.quantityModifier(-1)); document - .querySelector('.cb-increment-btn') + .querySelector('.cb-quantity .cb-increment-btn') .addEventListener('click', () => this.quantityModifier(1)); // Show blocks based on the options provided if (!this.options.showAddToCart) { this.toggleBlock('#cb-add-to-cart', true); + } else { + document + .querySelector('#cb-add-to-cart') + .addEventListener('click', () => { + CbCart.addProductToCart({ + itemId: this.widgetData.selectedFrequency.itemId, + itemPriceId: this.widgetData.selectedFrequency.id, + type: this.widgetData.selectedFrequency.type, + quantity: this.quantity, + productInfo: { + ...this.productInfo, + variantName: this.widgetData.selectedFrequency.variantName, + deliveryInfo: document.querySelector('.cb-delivery-interval') + .innerText, + price: this.widgetData.selectedFrequency.price + } + }); + }); } if (!this.options.showSubscribeNow) { this.toggleBlock('#cb-checkout', true); @@ -75,22 +94,30 @@ const CbWidget = { retrieveData: async function () { let error = null; // Fetch Variant, Plans, Charges info - const [variants, subscriptionPlans, oneTimeCharges] = await Promise.all([ - this.fetchCBApi('/api/variants?product_id=' + this.options.product_id), - this.fetchCBApi( - '/api/fetch-items?product_id=' + this.options.product_id + '&type=plan' - ), - this.fetchCBApi( - '/api/fetch-items?product_id=' + - this.options.product_id + - '&type=charge' - ) - ]).catch((err) => { - // Show Error message on the widget - this.showError(); - console.error(err); - error = err; - }); + const [product, variants, subscriptionPlans, oneTimeCharges] = + await Promise.all([ + this.fetchCBApi('/api/product?product_id=' + this.options.product_id), + this.fetchCBApi('/api/variants?product_id=' + this.options.product_id), + this.fetchCBApi( + '/api/fetch-items?product_id=' + + this.options.product_id + + '&type=plan' + ), + this.fetchCBApi( + '/api/fetch-items?product_id=' + + this.options.product_id + + '&type=charge' + ) + ]).catch((err) => { + // Show Error message on the widget + this.showError(); + console.error(err); + error = err; + }); + this.productInfo = { + name: product.name, + image: product?.metadata?.image + }; // Fetch Subscription price and one time prices const [subscriptionPrices, oneTimePrices] = await Promise.all([ this.fetchCBApi('/api/fetch-item-prices?item_id=' + subscriptionPlans.id), @@ -201,6 +228,9 @@ const CbWidget = { frequencies.forEach((frequency) => { const option = document.createElement('option'); option.value = frequency.id; + option.dataset.itemId = frequency.item_id; + option.dataset.variant = this.widgetData[variantId].name; + option.dataset.price = (frequency.price / 100).toFixed(2); // Construct frequency text let frequencyText = ''; if (frequency.period === 1) { @@ -221,7 +251,9 @@ const CbWidget = { frequency.shipping_period_unit }${frequency.shipping_period === 1 ? '' : 's'}`; } - option.dataset.description = `
${shippingText}
${frequency.description || ''}`; + option.dataset.description = `
${shippingText}
${ + frequency.description || '' + }`; frequencySelector.appendChild(option); }); frequencySelector.addEventListener('change', (e) => { @@ -232,9 +264,18 @@ const CbWidget = { changeFrequency: function (e) { this.widgetData.selectedFrequency = { id: e.target.value, - type: e.target.value?.endsWith(`-Charge-${this.options.currency}`) + itemId: document.querySelector( + '#cb-frequency [value="' + e.target.value + '"]' + )?.dataset?.itemId, + type: e.target.value?.endsWith(`-charge-${this.options.currency}`) ? 'charge' - : 'plan' + : 'plan', + variantName: document.querySelector( + '#cb-frequency [value="' + e.target.value + '"]' + )?.dataset?.variant, + price: document.querySelector( + '#cb-frequency [value="' + e.target.value + '"]' + )?.dataset?.price }; const subsDescription = document.querySelector('.cb-subs-description'); subsDescription.innerHTML = ''; @@ -245,13 +286,14 @@ const CbWidget = { }, subscribeNow: async function (e) { e.preventDefault(); + let url = `/api/generate_checkout_new_url?subscription_items[item_price_id][0]=${this.widgetData.selectedFrequency.id}&customer[id]=${this.options.customer_id}¤cy_code=${this.options.currency}&item_type=${this.widgetData.selectedFrequency.type}`; + if (this.widgetData.selectedFrequency.type !== 'flat_fee') { + url = `${url}&subscription_items[quantity][0]=${this.quantity}`; + } try { - const checkout = await this.fetchCBApi( - `/api/generate_checkout_new_url?subscription_items[item_price_id][0]=${this.widgetData.selectedFrequency.id}&subscription_items[quantity][0]=${this.quantity}&customer[id]=${this.options.customer_id}¤cy_code=${this.options.currency}&item_type=${this.widgetData.selectedFrequency.type}`, - { - method: 'POST' - } - ); + const checkout = await this.fetchCBApi(url, { + method: 'POST' + }); window.location.href = checkout.url; } catch (e) { console.error(e); @@ -277,8 +319,8 @@ const CbWidget = { } }; CbWidget.init({ - customer_id: 'CUSTOMER_ID', // Replace with Customer id - product_id: 'PRODUCT_ID', // Replace with product id + customer_id: 'aras_shaffer', // Replace with Customer id + product_id: 'Dog-food', // Replace with product id variantSelector: 'select', // select/button currency: 'USD' // 'USD', 'EUR', etc., });