UNPKG

@nuskin/ns-checkout

Version:

Ecomm3 Checkout module

1,229 lines (1,084 loc) • 95.8 kB
'use strict'; import '../app.js'; import {BuyerCookieService,UserService, AccountManager, AuthenticationService} from '@nuskin/ns-account'; import {ConfigService, RunConfigService, StringService, storage, events, StickyHeaderStack, StickyHeader, StickyHeaderBoundary, util, SponsorStorageService, StoreFrontSponsorStorageService, PersonalOfferStorageService} from '@nuskin/ns-util'; import {CartService, ShopContextService, LocalStorageOrderService, AdrConstants, AdrService, PaymentType, PaymentService, TransBankPaymentAdapter, CurrencyCodeToNumber, PersonalOfferService, CheckoutModeService} from '@nuskin/ns-shop'; import {AdrUtil,CartUtil,PickupUtil,ExternalPaymentStatusEnum,ModalUtil,OrderUtil,PaymentUtil,SectionUtil,ShippingAddressUtil,StringUtil,ThreatMatrixUtil} from '@nuskin/ns-checkout-common'; import {STATE_CONFIG} from "@nuskin/ns-form-elements"; angular.module('checkout').controller('OrderSummaryCtrl', [ '$scope', '$rootScope', '$filter', '$q', '$http', '$location', '$timeout', 'nsAdrOptions', 'nsShipping', 'nsTimeDelivery', 'nsPayment', 'nsProduct', 'nsOrder', 'nsUtil', 'nsSection', 'nsExternalPayment', 'nsSections', 'nsDowntime', 'tdcService', 'nsADRType', 'nsAdrUtil', 'nsGuestSignup', 'googleMapsApi', 'NgMap', function( $scope, $rootScope, $filter, $q, $http, $location, $timeout, nsAdrOptions, nsShipping, nsTimeDelivery, nsPayment, nsProduct, nsOrder, nsUtil, nsSection, nsExternalPayment, nsSections, nsDowntime, tdcService, nsADRType, nsAdrUtil, nsGuestSignup, googleMapsApi, NgMap) { /** * Request a check on the username (email). * * @param email * @return {Promise} * @private */ async function _duplicateUserCheck(email) { let domain = RunConfigService.getRunConfig().baseUrl; if (window.location.href.indexOf('localhost') !== -1) { domain = "https://test.nuskin.com"; } const DUPLICATE_USER_CHECK_URL = domain + "/account-util/api/v1/account/username-ck?login="; let response = await(fetch(DUPLICATE_USER_CHECK_URL + email, { method: "GET", headers: { 'client_id': ConfigService.getMarketConfig().checkout.clientId, 'client_secret': ConfigService.getMarketConfig().checkout.clientSecret, "Content-Type": "application/json" }, credentials: 'omit' })); return await(response.json()); } $scope.createAccount = false; /** * COM-1643 * * Convert boolean to either a 1 (for true) or 0 (for false). This * is beneficial for adding up boolean sums (e.g. password must meet 3 out of * 4 criteria). If something besides a boolean is passed in just convert that * to a zero or one with the default casting ruleset (truthy or falsy). */ function _boolToInt(bool) { return bool ? 1 : 0; } /** * COM-1643 * * The state of the account create. This takes into account the password and username (email) and is applied * towards error handling on account creation along with other fields in the form. * * Is the password invalid? Default is false. * Is the password empty? Default is true. * Do the passwords match? Default is true since by default password fields are empty (pristine). * What was the last value of the user for account creation? Default is an empty string. * Were the account creation fields validated? Default is false. * */ const ACCOUNT_CREATION_STATES_DEFAULTS = { isInvalidPassword: false, emptyPassword: true, emptyConfirmationPassword: true, lastAttemptedUser: "", passwordsDontMatch: false, validated: false, futureContactChecked: false, createAccountTermsConditionsChecked: false, contactByEmailChecked: false, contactBySMSChecked: false }; /** * Invoke this function * * @param key * @private */ function _resetAccountCreationState(key) { $scope.accountCreationStates[key] = (key) ? ACCOUNT_CREATION_STATES_DEFAULTS[key] : key; } /** * COM-1643 * * Set the account creation states from the default map. This function is used to either init or reset to the * account creation states. * * @private */ function _setAccountCreationStates() { if (!$scope.config.checkout.enablePolymerForms) { $scope.forms['preShippingForm'].showValidationError = false; } $scope.accountCreationStates = JSON.parse(JSON.stringify(ACCOUNT_CREATION_STATES_DEFAULTS)); } /** * COM-1643 invalidate the element * * @param element correlates to a form input element * @param key correlates to an error key * @private */ function _invalidate(element, key) { if (key) { element.$setValidity(key, false); } } /** * COM-1643 validate the element * * @param element correlates to a form input element * @param key correlates to an error key * @private */ function _validate(element, key) { element.$setValidity(key, true); } /** * COM-1643 * * Validate passwords for new account's created in this order: * * Ensure the length is greater than 8 * * AND * * Three out of the four conditions must be met: * * A special character * A lowercase character * An uppercase character * A number * * @return {Boolean} true if password is valid else false * @private */ function _validatePassword() { let password = angular.element("#userPasswordField").val(); let confirmationPassword = angular.element("#confirmationUserPasswordField").val(); $scope.accountCreationStates.strength = (_boolToInt(password && password.length >= 8)) && (_boolToInt(password && /[\@\#\$\%\^\&\*\(\)\_\+\!]/.test(password)) + _boolToInt(password && /[a-z]/.test(password)) + _boolToInt(password && /[A-Z]/.test(password)) + _boolToInt(password && /[0-9]/.test(password))); _setPasswordStates($scope.accountCreationStates.strength, password, confirmationPassword); return _setPasswordFormValidity(); } /** * COM-1643 * * Set the password state object. * * @param strength represents the strength of the password. Must exceed the criteria offset for a valid password. * @param password * @param confirmationPassword * @private */ function _setPasswordStates(strength, password, confirmationPassword) { const CRITERIA_STRENGTH_REQUIREMENT = 3; $scope.accountCreationStates.emptyPassword = !password; $scope.accountCreationStates.isInvalidPassword = !!password && (strength < CRITERIA_STRENGTH_REQUIREMENT); $scope.accountCreationStates.emptyConfirmationPassword = !confirmationPassword; // The passwords can only not match if the password and confirmation fields are populated (all or nothing): if (!$scope.accountCreationStates.emptyPassword && !$scope.accountCreationStates.emptyConfirmationPassword) { $scope.accountCreationStates.passwordsDontMatch = password !== confirmationPassword; } // It's ok to not have any text in either one of the password fields. If that's the case simply hide the // password's don't match error. else { $scope.accountCreationStates.passwordsDontMatch = false; } } /** * COM-1643 * * Set the form validity for the new account password. The order here is important so be careful when editing. * * @return {Boolean} true if valid password else false * @private */ function _setPasswordFormValidity() { let rv = false; // If no password was provided remove the password requirements error message // and just display the required message: if ($scope.accountCreationStates.emptyPassword) { _invalidate($scope.forms.preShippingForm.userPassword, "required"); // Technically the password isn't valid and isInvalidPassword should be true, but the required error above // is shown instead and accounts for the invalidity. $scope.accountCreationStates.isInvalidPassword = false; $scope.accountCreationStates.passwordsDontMatch = false; } // Else-if the password is provided but invalid remove the required message // and just display the password requirements error message: else if ($scope.accountCreationStates.isInvalidPassword) { _validate($scope.forms.preShippingForm.userPassword, "required"); $scope.accountCreationStates.isInvalidPassword = true; $scope.accountCreationStates.passwordsDontMatch = false; } // If a valid password has been provided but no confirmation password is provided display an error. else if ($scope.accountCreationStates.emptyConfirmationPassword) { $scope.accountCreationStates.isInvalidPassword = false; $scope.accountCreationStates.passwordsDontMatch = false; _invalidate($scope.forms.preShippingForm.confirmationUserPassword, "required"); } // If a valid password has been provided but the confirmation password doesn't equal the password display an error. else if ($scope.accountCreationStates.passwordsDontMatch) { $scope.accountCreationStates.isInvalidPassword = false; $scope.accountCreationStates.passwordsDontMatch = true; } // Else a valid password and confirmation password has been provided. else { $scope.accountCreationStates.isInvalidPassword = false; $scope.accountCreationStates.passwordsDontMatch = false; _validate($scope.forms.preShippingForm.userPassword, "required"); rv = true; } // Make sure the DOM gets the updated state variables apply(); return rv; } /** * COM-2282 toggle create account terms and conditions (with privacy policy) checkbox */ $scope.toggleCreateAccountTermsConditionsChecked = () => { if ($scope.createAccount) { $scope.accountCreationStates.createAccountTermsConditionsChecked = !$scope.accountCreationStates.createAccountTermsConditionsChecked; } else { // Reset: _resetAccountCreationState("createAccountTermsConditionsChecked"); } }; /** * COM-2282 toggle future contact checkbox */ $scope.toggleFutureContactChecked = () => { if ($scope.createAccount) { $scope.accountCreationStates.futureContactChecked = !$scope.accountCreationStates.futureContactChecked; // If checking "contact me", set email and SMS checkboxes to checked by default // If un-checking "contact me", set email and SMS checkboxes to unchecked $scope.accountCreationStates.contactByEmailChecked = $scope.accountCreationStates.futureContactChecked; $scope.accountCreationStates.contactBySMSChecked = $scope.accountCreationStates.futureContactChecked; } else { // Reset: _resetAccountCreationState("futureContactChecked"); } }; /** * COM-2282 toggle contact-by-email checkbox */ $scope.toggleContactByEmailChecked = () => { if ($scope.createAccount && $scope.accountCreationStates.futureContactChecked) { $scope.accountCreationStates.contactByEmailChecked = !$scope.accountCreationStates.contactByEmailChecked; } else { // Reset: _resetAccountCreationState("contactByEmailChecked"); } }; /** * COM-2282 toggle contact-by-sms checkbox */ $scope.toggleContactBySMSChecked = () => { if ($scope.createAccount && $scope.accountCreationStates.futureContactChecked) { $scope.accountCreationStates.contactBySMSChecked = !$scope.accountCreationStates.contactBySMSChecked; } else { // Reset: _resetAccountCreationState("contactBySMSChecked"); } }; /** * COM-1643 * * Toggle the create account fields. The Angular code in the orderSummary.html will update the fields based off of * the value set on $scope.createAccount. Password is always required if visible whereas the email may or may not * be required if visible (depends on if the create account checkbox is checked). If the order has an ADR item then * the password and username must be visible and required. */ $scope.toggleCreateAccount = (createAcct) => { $scope.createAccount = createAcct; let shippingEmailLabel = angular.element("label[for=shippingEmail]"); let shippingEmailInput = angular.element("input[name=shippingEmail]"); if (shippingEmailLabel) { shippingEmailLabel.toggleClass("nuskinRequiredBefore", $scope.createAccount || $scope.orderHasAdr); // Set required attributes: if(shippingEmailLabel.hasClass("nuskinRequiredBefore")) { shippingEmailInput.prop("required", "true"); } else { shippingEmailInput.removeAttr("required"); } _validateEmail().then(() => {}).catch(() => {}); } if ($scope.createAccount) { $scope.sections.shipping.newAddress.saveShipping = true; $scope.sections.payment.newPayment.savePayment__hide = false; $scope.sections.payment.newPayment.defaultPayment__hide = false; } else { $scope.sections.shipping.newAddress.saveShipping = false; $scope.sections.payment.newPayment.savePayment__hide = true; $scope.sections.payment.newPayment.defaultPayment__hide = true; } }; /** * COM-1643 * * Tread lightly! * * This nest of boolean logic sets and unsets the error states and messages pertaining to the email (username). * Promise issues a resolve if a valid email is provided and error messages are removed else set one or more error * messages and issue a promise rejection. * * @private * @return {Promise.<*>} */ function _validateEmail() { let shippingEmailInput = angular.element("input[name=shippingEmail]"); let shippingEmailRequired = angular.element("div[id=shippingEmailRequired]"); let shippingEmailAlreadyTaken = angular.element("div[id=shippingEmailAlreadyTaken]"); let shippingEmailInputLength = shippingEmailInput.val().length; // If the email is required: if (shippingEmailInput.attr("required")) { // If the email field is required and no text was provided print an error message and reject: if (shippingEmailInputLength <= 0) { let markup = `<div id='shippingEmailRequired'><br><span class='orderSummaryError'>${$scope.tdc.requiredFieldLabel}</span></div>`; // Make sure not to re-add the markup if (shippingEmailRequired.length <= 1) { shippingEmailInput.after(markup); } $scope.forms.preShippingForm.shippingEmail.$setValidity("invalid", true); return Promise.reject("Shipping email is required but no email was provided."); } // Else if there is text and the email is valid (matches the pattern) ensure it's not a duplicate: else if ($scope.forms.preShippingForm.shippingEmail.$valid) { return new Promise((resolve, reject) => { return _duplicateUserCheck(shippingEmailInput.val()) .then((data) => { if (!data.available) { let markup = `<div id='shippingEmailAlreadyTaken'><br><span class='orderSummaryError'>${$scope.tdc.userAlreadyTaken}</span></div>`; // Make sure not to re-add the markup if ($("div[id=shippingEmailAlreadyTaken]").length <= 1) { shippingEmailInput.after(markup); } $scope.accountCreationStates.lastAttemptedUser = shippingEmailInput.val(); $scope.forms.preShippingForm.shippingEmail.$setValidity("invalid", true); shippingEmailRequired.remove(); $scope.forms.preShippingForm.shippingEmail.$setValidity("required", true); reject("Shipping email can't be applied to account creation because it's already taken."); } else { shippingEmailAlreadyTaken.remove(); shippingEmailRequired.remove(); $scope.forms.preShippingForm.shippingEmail.$setValidity("required", true); } resolve(); }) .catch(console.error); }); } // Else it seems that there is some text but the email is still invalid. Remove the required error message // as there is something but leave the invalid message: else { shippingEmailRequired.remove(); $scope.forms.preShippingForm.shippingEmail.$setValidity("required", true); return Promise.reject("Shipping email provided but in an invalid format."); } } // If the email field isn't required remove any required errors: else { shippingEmailRequired.remove(); angular.element("div[id=shippingEmailAlreadyTaken]").remove(); $scope.forms.preShippingForm.shippingEmail.$setValidity("required", true); } return Promise.resolve(); } $scope.getItemCnt = () => { return $scope.order.sapItemQty; }; $scope.getAddressSummary = (address) => { let result = ""; if (address) { result = address.shippingAddress1; if (address.shippingAddress2) { result += `, ${address.shippingAddress2}`; } if (address.shippingCounty) { result += `, ${address.shippingCounty}`; } if (address.addressType != '7-11') { result += `, ${address.shippingCity}`; } if (address.shippingState) { result += `, ${$filter('stateName')(address.shippingState)}`; } if (!$scope.config.checkout.hideShippingPostalCode) { result += `, ${address.shippingPostalCode}`; } if (address.shippingCountry) { result += `, ${address.shippingCountry}`; } } return result; }; $scope.shippingAccordionClicked = () => { // Shipping section is open // If we are busy don't let them do anything if (!$scope.isBusy && !$scope.sections.shipping.pristine) { if ($scope.sections.shipping.edit && $scope.order.selectedAddress) { // If we are id edit mode don't let them try to continue if we are adding or have errors if (!$scope.sections.shipping.adding && !$scope.sections.shipping.editing && !$scope.sections.shipping.error.hasErrorMessages()) { // If they changed the address then we need to simulate and force the opening of the shipmethod section if (!$scope.currentSelectedAddress || $scope.currentSelectedAddress.beShippingAddressId !== $scope.order.selectedAddress.beShippingAddressId) { $scope.sections.shipmethod.pristine = true; $scope.changeShippingAddressAndSimulate(); } $scope.continueEdit('shipping'); } } else if ($scope.enableChangeShippingAddress) { // When the section is opened set the currentSelectedAddress so we can compare when it is closed to force a simulate $scope.currentSelectedAddress = $scope.order.selectedAddress; $scope.updateEdit('shipping'); } } }; $scope.shipMethodAccordionClicked = () => { if (!$scope.isBusy && !$scope.sections.shipmethod.pristine) { if ($scope.sections.shipmethod.edit) { if (!$scope.shouldSelectPickupPoint() && !$scope.sections.shipmethod.error.hasErrorMessages()) { //Collapse accordion $scope.continueEdit('shipmethod'); } } else if ($scope.enableChangeShippingAddress) { $scope.updateEdit('shipmethod'); } } }; $scope.billingAccordionClicked = () => { if (!$scope.isBusy && !$scope.sections.payment.pristine) { if ($scope.sections.payment.edit) { if (!$scope.isPaymentContinueBtnDisabled()) { $scope.continueEdit('payment'); } } else { $scope.updateEdit('payment'); } } }; $scope.adrAccordionClicked = () => { if (!$scope.isBusy && !$scope.sections.adrOptions.pristine) { if ($scope.sections.adrOptions.edit) { $scope.continueEdit('adrOptions'); } else { $scope.updateEdit('adrOptions'); } } }; $scope.toggleFlag = (flag) => { this[flag] = !(!!flag); }; /** * COM-1643 * * Decorator for the ShippingAddressUtil addShippingAddress. * * @param type * @param element */ $scope.addShippingAddressOrderSummary = (type, element, disabled = false) => { if (disabled) { return; } if ($scope.config.checkout.enablePolymerForms) { document.getElementById('preShippingFormWithCreate').validate(); } else { $scope.forms[type + "ShippingForm"].showValidationError = true; } // For a user to create the account they must be in a guest checkout flow and either the create account checkbox // is checked or the order has an ADR. if ($scope.isGuestCheckout && ($scope.createAccount || $scope.orderHasAdr)) { // User didn't check the Terms and Conditions and Privacy Policy checkbox: if (!$scope.accountCreationStates.createAccountTermsConditionsChecked) { return; } if (!$scope.hideGovtIdfield && !$scope.sections.shipping.newAddress.govtID) { $scope.forms.preShippingForm.govtID.$error.required = true; return; } _validateEmail().then(() => { if ($scope.createAccount) { if (!_validatePassword()) { return; } // The password and email (username) are valid; submit the form data: $scope.addShippingAddress(false, type, element, $scope.sections.shipping.newAddress.govtID, $scope.sections.shipping.newAddress.userPassword, $scope.accountCreationStates.contactBySMSChecked, $scope.accountCreationStates.contactByEmailChecked); } }).catch(() => {}); } else if ($scope.isGuestCheckout) { if (!$scope.hideGovtIdfield && !$scope.sections.shipping.newAddress.govtID) { if (!$scope.config.checkout.enablePolymerForms) { $scope.forms.preShippingForm.govtID.$error.required = true; } return; } else { $scope.addShippingAddress(false, type, element, $scope.sections.shipping.newAddress.govtID); } } else { // Else add the shipping address. The password isn't embedded but that's fine since this isn't a create // account (guest checkout with or without ADR) flow. $scope.addShippingAddress(false, type, element); } }; $scope.resetShippingAccountState = () => { _setAccountCreationStates(); }; // Returns true if the product's price list includes a points price and the item is not ADR, false otherwise $scope.hasPointsPrice = item => (Number.isFinite(item.points) && !item.isAdr); // Returns the total points price for this item // (e.g. item that costs 4.5 pts each, user is purchasing 3 with points -> item points subtotal = 4.5 * 3 = 13.5 pts) $scope.getItemSubtotalPoints = item => item.qtyRedeemWithPoints * item.points; $scope.getItemUnitPrice = sapItem => { let unitPrice = 0; if (sapItem.price > 0) { // calculate based off of simulate. Should not be a divide by 0 since lineItem price > 0. (qty > qtyRedeemWithPoints unitPrice = sapItem.price / (sapItem.qty - sapItem.qtyRedeemWithPoints); } else { // can't base it off of simulate, use cart unit price. CartService.getItemData({cartItems: true}) .filter(item => item.sku === sapItem.sku) .forEach(item => { unitPrice = item.price; }); } return unitPrice; }; // Handle points allocation for line items with quantity = 1 $scope.syncSingleItemRedeem = item => { if ($scope.showPointsPaymentOptions && validatePointsChange(item, item.redeem ? item.qty : 0)) { updateDomItemPricing(item, item.redeem ? item.qty : 0); $scope.updateEdit("cart"); } else { item.redeem = false; } }; // Handle points allocation for line items with quantity > 1 $scope.updatePtsForMultipleQtyItem = (item, usePoints) => { if ($scope.showPointsPaymentOptions && validatePointsChange(item, usePoints ? 1 : 0)) { if (usePoints) { item.redeem = true; updateDomItemPricing(item, 1); } else { item.redeem = false; updateDomItemPricing(item, 0); } $scope.updateEdit("cart"); } else { item.redeem = false; } }; $scope.decreaseQtyRedeem = item => { if ($scope.showPointsPaymentOptions && validatePointsChange(item, item.qtyRedeemWithPoints - 1)) { if (item.qtyRedeemWithPoints > 1) { updateDomItemPricing(item, item.qtyRedeemWithPoints - 1); } } }; $scope.increaseQtyRedeem = item => { if ($scope.showPointsPaymentOptions && validatePointsChange(item, item.qtyRedeemWithPoints + 1)) { if (item.qtyRedeemWithPoints < item.qty) { updateDomItemPricing(item, item.qtyRedeemWithPoints + 1); } } }; /** * This is the ng-click event function for the points Continue button. * Simulate order then reset (un-pristine) the points fields. */ $scope.pointsContinueClicked = (disabled = false) => { if (disabled) return; if ($scope.pointsEdited) { $scope.simulateOrder().then(function () { $scope.continueEdit("cart"); $scope.pointsEdited = false; }); } else { $scope.continueEdit("cart"); } }; $scope.updateQtyRedeem = item => { if (!$scope.showPointsPaymentOptions || !item.redeem) { return; } let newQtyRedeem = Math.floor(item.qtyRedeemWithPoints || 0); newQtyRedeem = Math.floor(newQtyRedeem || 0); newQtyRedeem = newQtyRedeem > item.qty ? item.qty: newQtyRedeem; newQtyRedeem = newQtyRedeem < 1 ? 1 : newQtyRedeem; if (validatePointsChange(item, newQtyRedeem)) { updateDomItemPricing(item, newQtyRedeem); } }; // Sets the quantity to redeem with points on the item, and updates the item // price, cart subtotal price, and grand total price accordingly. function updateDomItemPricing(sapItem, qtyRedeem) { CartService.getItemData({cartItems: true}) .filter(cartItem => cartItem.sku === sapItem.sku) .forEach(cartItem => { let {deltaItemTotal, sapItemData} = CartService.updateItemPointsPrice(sapItem.key, cartItem.key, qtyRedeem); // update local sapItem with new information Object.assign(sapItem, sapItemData); $scope.order.orderTotals.itemSubtotal += deltaItemTotal; $scope.order.orderTotals.grandTotal += deltaItemTotal; $scope.cartPointsSubtotal = getPointsPriceTotal(false); $scope.pointsTotal = getPointsPriceTotal(); }); $scope.order.orderTotals.psvTotal = getPsvTotal(); } function getPsvTotal() { let psvRunningTotal = 0; CartService.getItemData({sapItems: true}) .forEach(sapItem => { // update psv psvRunningTotal += (sapItem.qty - sapItem.qtyRedeemWithPoints) * CartService.getItemUnitPsv(sapItem.key); }); return psvRunningTotal; } function validatePointsChange(sapItem, newQtyRedeem) { let pointsDiff = (sapItem.points * newQtyRedeem) - (sapItem.points * (sapItem.qtyRedeemWithPoints || 0)), singlePsv = CartService.getItemUnitPsv(sapItem.key), psvDiff = ((singlePsv * newQtyRedeem) - (singlePsv * sapItem.qtyRedeemWithPoints || 0)).toFixed(2), retVal = false; $scope.pointsEdited = true; // Skip validation if user's points balance not yet initialized if (!$scope.showPointsPaymentOptions || $scope.userAvailablePoints === null) { return; } if ($scope.pointsTotal + pointsDiff > $scope.userAvailablePoints) { addValidationMessage($scope.tdc.userPointsExceededError); } else if (!AdrService.isAdrOverride() && getOrderTotalPsv() - psvDiff <= 0) { addValidationMessage($scope.tdc.zeroPsvError); } else { removeValidationMessage(); retVal = true; } function addValidationMessage(message) { let domHasMessage = $scope.sections.cart.error.infoMessages .some(msg => msg.text === message); if (!domHasMessage) { $scope.sections.cart.error.addErrorMessage('info', {text: message}); } } function removeValidationMessage() { let leftOverMessages = $scope.sections.cart.error.infoMessages .filter(item => item.text !== $scope.tdc.userPointsExceededError && item.text !== $scope.tdc.zeroPsvError); $scope.sections.cart.error.infoMessages = []; leftOverMessages.forEach(message => $scope.sections.cart.error.addErrorMessage('info', {text: message})); } return retVal; } function getOrderTotalPsv() { return parseFloat(CartService.getItemData({cartItems: true}) .map(item => { return item.singlePv * (item.qty - (item.qtyRedeemWithPoints || 0)); }) .reduce((a, b) => a + b, 0)) .toFixed(2); } // Because the checkout DOM is built from SAP items list, and order requests are built from cart items list, // this function copies changes to points allocation from SAP item to cart item function syncCartItemToSapItem(sapItem) { CartService.syncCartItemToSapItem(sapItem.key); } /*** * Acquire the total points. * * @param applyShipping{boolean} to the total point price. True by default. * @returns {number} the point price total */ function getPointsPriceTotal(applyShipping = true) { if (!$scope.order) { return 0.0; } let productsTotal = CartService.getItemData({sapItems: true}) .filter(item => !!item.points) .map(item => { let qtyRedeem = item.qtyRedeemWithPoints > 0 ? item.qtyRedeemWithPoints : item.redeem ? item.qty : 0; return item.points * qtyRedeem; }) .reduce((a, b) => a + b, 0); let pointsShippingCost = 0; if (nuskin.configuration.adpManage && nuskin.configuration.adpManage.PayWithPointsShippingCost) { pointsShippingCost = Number.parseInt(nuskin.configuration.adpManage.PayWithPointsShippingCost); } return productsTotal + (($scope.order.shippingPayWithPoints && applyShipping) ? pointsShippingCost : 0); } function resetPointsAllocations() { if ($scope.showPointsPaymentOptions) { CartService.getItemData({sapItems: true}) .filter(item => item.redeem) .forEach(item => { updateDomItemPricing(item, 0); }); $scope.order.shippingPayWithPoints = false; } } $scope.isPWPShippingMethod = selectedMethod => { let adpManage = nuskin.configuration.adpManage; if (!adpManage || !adpManage.enablePayWithPoints || !adpManage.PayWithPointsShippingMethods) return false; if (!selectedMethod || !selectedMethod.Code) return false; return adpManage.PayWithPointsShippingMethods.indexOf(selectedMethod.Code) > -1; }; $scope.onTogglePWPShipping = () => { $scope.pointsTotal = getPointsPriceTotal(); if ($scope.pointsTotal > $scope.userAvailablePoints) { $scope.order.shippingPayWithPoints = false; $scope.pointsTotal = getPointsPriceTotal(); // Add user error message if not already present let hasPtsExceededError = $scope.sections.review.error.errorMessages .some(msg => msg.text === $scope.tdc.userPointsExceededError); if (!hasPtsExceededError) { $scope.sections.review.error.addErrorMessage("error", {text: $scope.tdc.userPointsExceededError}); } } }; $scope.gmap = { googleMapsApiUrl: googleMapsApi, markers: null, selectedMarker: null, instance: null }; $scope.showCheckout = storage.getItem(storage.metadata.RECEIVING_ITEMS) !== true; var EXTERNAL_PAYMENT_NEXT_ORDER = "externalPaymentNextOrder"; if (!$scope.showCheckout) { return; } $scope.forms = {}; $scope.runConfig = RunConfigService.getRunConfig(); // Provide a default landing page if needed. if (!$scope.runConfig.landing){ $scope.runConfig.landing = "checkout.html#/orderSummary"; } $scope.buyer = BuyerCookieService.getBuyer(); $scope.user = UserService.getUser(); // See: checkout/harness/index.html (search for checkoutSetup) try { $scope.editMode = checkoutSetup.editMode; $scope.bruneiName = checkoutSetup.bruneiName; $scope.indonesiaName = checkoutSetup.indonesiaName; $scope.malaysiaName = checkoutSetup.malaysiaName; $scope.philippinesName = checkoutSetup.philippinesName; $scope.singaporeName = checkoutSetup.singaporeName; $scope.thailandName = checkoutSetup.thailandName; } // If checkoutSetup is not set up: catch(err) { console.error(err.message); } $scope.saveSAPAddresstoProfile = false; $scope.transBankInstallmentsBusy = false; $scope.transBankErrorMessage = ""; $scope.isSummerPromo = false; $scope.getCartUrl = function () { if ($scope.runConfig.cartUrl && $scope.runConfig.cartUrl.length > 0) { return $scope.runConfig.cartUrl; } else { var cart = ""; if (StoreFrontSponsorStorageService.getStoreFrontSponsor() && SponsorStorageService.getSponsor()) { cart = 'cart.mysite.html#/cart'; } else { cart = 'cart.html#/cart'; } return cart; } }; if (nsAdrUtil.showAdrShipWhen()) { $location.path("/adrShipWhen"); return; } // --------------------------------------------- // // Private Variables // // --------------------------------------------- var buyer = $scope.buyer; var user = $scope.user; // --------------------------------------------- // // Public Variables // // --------------------------------------------- $scope.hideGovtIdfield = true; $scope.config = ConfigService.getMarketConfig(); ConfigService.loadCustomJSStylesOnPage('Checkout'); if ($scope.config.signup.governmentIDOptions !== "NONE" && $scope.config.signup.governmentIDOptions !== undefined) { $scope.hideGovtIdfield = false; } if (nuskin.util.MobileUtils.isMobileLike() || (window.innerWidth < 1337 && $scope.config.checkout.usecheckoutSideBar)) { $scope.pickupMapSelected = true; } $scope.tdc = tdcService.getTdc('checkout'); $scope.PaymentType = PaymentType; $scope.hasEventItems = CartService.hasEventItems(); $scope.isPitchCart = !!CartService.getCartProperty("isPitchCart"); if (StoreFrontSponsorStorageService.getStoreFrontSponsor() && SponsorStorageService.getSponsor() && !$scope.isPitchCart) { $scope.isStorefrontCart = true; } else { $scope.isStorefrontCart = false; } $scope.isGuestCheckout = CheckoutModeService.isGuestCheckout(); $scope.hidePointValue = false; $scope.isGift = !!CartService.getCartProperty("isGift"); $scope.chosen = {}; $scope.chosen.element = null; $scope.chosen.isValid = false; $scope.termsConditionsCheckbox = false; $scope.globalHeader = null; $scope.errorHeader = null; $scope.paymentIsWire = false; $scope.deleteShippingDialogId = null; $scope.deletePaymentDialogId = null; $scope.emailOptIn = false; $scope.stickySideBar = { value: false }; $scope.slideUpOrderReview = false; $scope.isIOS = nuskin.util.MobileUtils.isIOS(); $scope.googleMapsChanged = false; // The following properties on scope should not be modified in a isolate scope directive $scope.contentLoaded = false; $scope.showSpinner = true; $scope.buyForOther = false; $scope.isBusy = false; $scope.forceDisableOrderButton = false; $scope.backdatingToggle = false; $scope.showExtraPaymentInfo = false; $scope.usedPoints = CartService.getCartInfo().totalOrderPoints; $scope.showPsv = ShopContextService.showPsv($scope.buyer ? $scope.buyer.accountType : 0) && !$scope.isGuestCheckout; $scope.showPointsPaymentOptions = false; $scope.detailsIcon = 'nuskin:icon-smallcart-empty'; $scope.userAvailablePoints = null; $scope.pointsTotal = 0; // Health warnings required by California for orders containing applicable SKUs $scope.californiaWarnings = null; if ($scope.isPitchCart) { $scope.personalOffer = PersonalOfferStorageService.getPersonalOffer(); } SectionUtil.initSections($scope, nsSections, nsAdrUtil, $timeout, PersonalOfferService); var hideProcessingIndicators = function() { $('#mobile-processing').hide(); $scope.isBusy = false; }; // If authentication is required and the user is not authenticated we redirect back to the cart with a force login. if (ConfigService.getMarketConfig().authenticationRequired) { var redirectToCartPage = false; if (UserService.isLTOSite()){ if (!AuthenticationService.isLoggedIn()){ redirectToCartPage = true; } } else if (!AccountManager.isLoggedIn() && !$scope.isGuestCheckout){ redirectToCartPage = true; } if (redirectToCartPage){ var indexOfCheckout = window.location.href.indexOf('checkout.html'); window.location.href = window.location.href.substr(0, indexOfCheckout) + $scope.getCartUrl() + "#forceLogin"; return; } } // Redirect to order confirmation if we have received a successful externalPayment var needToHandleExternalPayment = nsExternalPayment.needToHandleExternalPayment(); if (needToHandleExternalPayment) { hideProcessingIndicators(); if (nsExternalPayment.handlePayment($scope.tdc, $scope.sections)) { hideProcessingIndicators(); return; // exit here... skip the rest of the functions } } // --------------------------------------------- // // Public Methods // // --------------------------------------------- $scope.initAndGo = function () { $scope.isBusy = true; var simulatePromiseDependencies = []; OrderUtil.initOrders($scope, $rootScope, $location, $timeout, $q, $http, nsOrder, nsProduct, nsUtil, nsExternalPayment, simulatePromiseDependencies, nsGuestSignup); PaymentUtil.initPayments($scope, $http, nsUtil, nsPayment, nsAdrUtil, simulatePromiseDependencies, PersonalOfferService); // Special case for JP with Business Portfolio.... if ($scope.order.simulateWithoutShipping) { CartUtil.initializeCart(); // For fixing the P1 production issue $('#orderSummaryRoot').css('display', 'block'); } else { ShippingAddressUtil.initShippingAddresses($scope, nsUtil, nsShipping, nsTimeDelivery, $timeout, simulatePromiseDependencies, nsGuestSignup, PersonalOfferService); loadCheckout(); } $scope.cuotas = $scope.order.selectedPayment && $scope.order.selectedPayment.useInstallments ? $scope.order.selectedPayment.getInstallmentSelectedNumber() : 1; setTimeout(function () { if (nuskin && ConfigService.getMarketConfig().enableThreatMetrics) { ThreatMatrixUtil.initThreatMatrixScriptUrl($scope, nsUtil); } }, 2000); }; $scope.initShippingAddresses = function() { ShippingAddressUtil.initShippingAddresses($scope, nsUtil, nsShipping, nsTimeDelivery, $timeout, []); }; $scope.setCaliforniaWarnings = function() { let address = $scope.order.selectedAddress; if (!address) return; // exit early if there is no shipping address if ($scope.runConfig.country === 'US' && address.shippingState === 'CA') { // Order is shipping to California; add warnings if order contains applicable SKUs; let skuWarnings = window.californiaSkuWarnings || []; let orderItems = CartService.getItemData({sapItems: true}); $scope.californiaWarnings = skuWarnings .filter(s => orderItems.some(item => item.sku === s.sku)) .map(s => s.warning) .join('\n'); } else { $scope.californiaWarnings = null; } }; /** * Load sticky headers and hide processing indicators once content has been loaded into DOM */ $scope.$watch("contentLoaded", function(newValue) { if(newValue) { $scope.enableCreateAccount = nuskin.configuration.signup.enableCreateAccount; $scope.buildStickyHeaders(); hideProcessingIndicators(); $scope.pointsEdited = false; showPointsPaymentOptions(); _setAccountCreationStates(); /** * COM-1921 If there is an ADR item on the order then we need to require the account to be created and we * need to create the account earlier in the process. */ let shippingEmailLabel = angular.element("label[for=shippingEmail]"); let shippingEmailInput = angular.element("input[name=shippingEmail]"); shippingEmailInput.blur(() => { _validateEmail().then(() => {}).catch(() => {}); }); let userPasswordField = angular.element("#userPasswordField"); let confirmationUserPasswordField = angular.element("#confirmationUserPasswordField"); userPasswordField.blur(() => _validatePassword()); confirmationUserPasswordField.blur(() => _validatePassword()); if (CartService.getItemCnt({cartAdrItems: true}) > 0) { $scope.createAccount = !!$scope.isGuestCheckout; if (!shippingEmailLabel.hasClass("nuskinRequiredBefore")) { shippingEmailLabel.attr("required", ""); shippingEmailLabel.addClass("nuskinRequiredBefore"); } else { shippingEmailLabel.removeAttr("required"); } // Set required attributes: if(shippingEmailLabel.hasClass("nuskinRequiredBefore")) { shippingEmailInput.attr("required", true); } else { shippingEmailInput.removeAttr("required"); } $scope.orderHasAdr = true; } else { $scope.orderHasAdr = false; shippingEmailLabel.removeAttr("required"); shippingEmailInput.removeAttr("required"); } $scope.setCaliforniaWarnings(); $scope.sidebar = document.getElementById("checkoutSideBar"); if (nuskin.util.MobileUtils.isMobileLike()) { window.requestAnimationFrame(stickyScroll); } } }); function simulateWithoutShippingForJapan() { $scope.order.simulateWithoutShipping = true; $scope.saveSAPAddresstoProfile = true; CartUtil.initializeCart(); // For fixing the P1 production issue $('#orderSummaryRoot').css('display', 'block'); } $scope.buildStickyHeaders = function () { setTimeout(function () { if (nuskin && StickyHeader && !$scope.busyHeader) { // If we're inside AgeLoc, we need to watch a different scroll element if(document.querySelector('.ageloc-wrap')) { StickyHeaderStack.scrollWatch= '.ageloc-wrap'; } StickyHeaderStack.reinitialize(); var headerBoundary = new StickyHeaderBoundary('#header'); StickyHeaderStack.stackOffsetMobile = '#header'; $scope.errorHeader = new StickyHeader('#checkoutErrorSummary .errorDialogue', { customLeftAlignment: true, onClick: 'scrollToOrigin' }); $scope.shippingError = new StickyHeader('#checkout-shipping-address .errorDialogue', { customLeftAlignment: true, onClick: 'scrollToOrigin' }); $scope.paymentError = new StickyHeader('#checkout-edit-payment .errorDialogue', { customLeftAlignment: true, onClick: 'scrollToOrigin' }); $scope.busyHeader = new StickyHeader('#busyHeader', { customLeftAlignment: true, onClick: 'scrollToOrigin' }); $scope.reviewError = new StickyHeader('#checkout-review-order .errorDialogue', { customLeftAlignment: true, onClick: 'scrollToOrigin' }); StickyHeaderStack.$stackProxy.addClass('ns-atomic'); } }, 5000); }; function stickyScroll() { let sidebar = document.getElementById("checkoutSideBar"); if (sidebar) { let sticky = sidebar.getBoundingClientRect().top; let paymentSection = document.getElementById("checkout-edit-payment"); let paymentBottom = paymentSection.getBoundingClientRect().bottom; let adrSection = document.getElementById("checkout-adrOptions"); let adrBottom = adrSection.getBoundingClientRect().bottom; let bottomSection = paymentBottom; let body = document.getElementsByTagName('body')[0]; let windowWidth = body.offsetWidth; let windowHeight = window.innerHeight; if ($scope.order.adr || $scope.adr) { bottomSection = adrBottom; } if (nuskin.util.MobileUtils.isMobileLike()) { sidebar.removeAttribute('style'); if (windowHeight < windowWidth && windowWidth >= 767) { $scope.stickySideBar.value = false; } else if ((windowHeight - 60) < sticky && !$scope.stickySideBar.value) { $scope.stickySideBar.value = true; } else if ((windowHeight - 60) <= sticky && $scope.stickySideBar.value && bottomSection > (windowHeight - 70)) { return } else if (bottomSection < (windowHeight - 70)) { $scope.stickySideBar.value = false; $scope.slideUpOrderReview = false; } if ($scope.$$phase !=