/***************************************************************************** Component Name : ShopifyCredentials Created Date : 17/4/2024 @description : This component manages the configuration of Shopify stores by handling the creation of new, editing, and deleting actions, as well as managing currency and pricebook settings. @author : SME Team Modification Log: Ver Date Author 1.0 30/4/2024 SME Team *****************************************************************************/ import { LightningElement, track, api, wire } from 'lwc'; // Auto Configuration import { getRecord } from 'lightning/uiRecordApi'; import USER_EMAIL from '@salesforce/schema/User.Email'; import USER_ID from '@salesforce/user/Id'; // FETCHING RESOURCES FROM THE STATIC RESOURCES // import pageLoadingGif from "@salesforce/resourceUrl/SMEPageLoadingGif"; import storeulgetinfo from '@salesforce/resourceUrl/storeulgetinfo'; import storepassword from '@salesforce/resourceUrl/Storepassword'; import Resources from "@salesforce/resourceUrl/Resources"; import SMEPageLoadingGIF2 from "@salesforce/resourceUrl/SMEPageLoadingGIF2"; import syncLogo1 from '@salesforce/resourceUrl/syncLogo1'; import SMELOGO from '@salesforce/resourceUrl/SMELOGO'; import StoreLogo from '@salesforce/resourceUrl/StoreLogo'; import SFLogo from '@salesforce/resourceUrl/SFLogo'; // CREATING INSTANCES OF APEX METHODS //import getConfigration from '@salesforce/apex/ShopifyController.getConfigration'; import getSiteURLs from '@salesforce/apex/ShopifyController.getSiteURLs'; import getActivePricebook from '@salesforce/apex/ShopifyController.getActivePricebook'; import getErrorMailList from '@salesforce/apex/ShopifyController.getErrorMailList'; import RemoveStore from '@salesforce/apex/ShopifyController.removeStore'; import getConfigrations from '@salesforce/apex/ShopifyController.getConfigrations'; import getConfigrationForDynamicRow from '@salesforce/apex/ShopifyController.getConfigrationForDynamicRow'; import saveEmailSettings from '@salesforce/apex/ShopifyController.saveEmailSettings'; import getActiveCurrencies from '@salesforce/apex/ShopifyController.getActiveCurrencies'; import createRemoteSiteSettings from '@salesforce/apex/ShopifyController.createRemoteSiteSettings'; import checkCreds from '@salesforce/apex/ShopifyController.checkCreds'; import checkAccesScope from '@salesforce/apex/ShopifyController.checkAccesScope'; import checkStoreNameRemoteSite from '@salesforce/apex/ShopifyController.checkStoreNameRemoteSite'; import getOrgEnvironment from '@salesforce/apex/ShopifyController.getOrgEnvironment'; import initShopifyTokenGeneration from '@salesforce/apex/ShopifyController.initShopifyTokenGeneration'; //Add new impor for default site url import getDefaultSiteUrl from '@salesforce/apex/ShopifyController.getDefaultSiteUrl'; // New Method to imrpove performance import updatePriceBookCurrencyStage from '@salesforce/apex/ShopifyController.updatePriceBookCurrencyStage'; // DISPLAY-ONLY: last synced order timestamp for the hero "Last order synced X ago" text import getLastOrderSyncTime from '@salesforce/apex/ShopifyController.getLastOrderSyncTime'; import getOrganizationName from '@salesforce/apex/ShopifyController.getOrganizationName'; //CUSTOM LABELS import Product from '@salesforce/label/c.Product'; import Customer from '@salesforce/label/c.Customer'; import Draft_Order from '@salesforce/label/c.Draft_Order'; import Order from '@salesforce/label/c.Order'; import Collection from '@salesforce/label/c.Collection'; import Inventory from '@salesforce/label/c.Inventory'; import Location from '@salesforce/label/c.Location'; import Please_enter_your_store_name from '@salesforce/label/c.Please_enter_your_store_name'; import Get_your_Shopify_password_from_private_app from '@salesforce/label/c.Get_your_Shopify_password_from_private_app'; import THIS_ACTION_REQUIRES_MERCHANT_APPROVAL from '@salesforce/label/c.THIS_ACTION_REQUIRES_MERCHANT_APPROVAL'; import Select_a_Site_URL from '@salesforce/label/c.Select_a_Site_URL'; import Generate_Access_Token_Info from '@salesforce/label/c.Generate_Access_Token_Info'; import Salesforce_Sites_enables_you_to_create_public from '@salesforce/label/c.Salesforce_Sites_enables_you_to_create_public'; import click_here from '@salesforce/label/c.click_here'; import Shopify_Store_Name from '@salesforce/label/c.Shopify_Store_Name'; import The_standard_price_book_is_the_master_list from '@salesforce/label/c.The_standard_price_book_is_the_master_list'; import Select_a_Currency from '@salesforce/label/c.Select_a_Currency'; import Multicurrency from '@salesforce/label/c.Multicurrency'; import Default_Currency from '@salesforce/label/c.Default_Currency'; import Default_Organization_Currency_ISO_Code from '@salesforce/label/c.Default_Organization_Currency_ISO_Code'; import Real_time_Sync from '@salesforce/label/c.Real_time_Sync'; import Some_objects_are_mandatory from '@salesforce/label/c.Some_objects_are_mandatory'; import Please_add_email_for_notifications from '@salesforce/label/c.Please_add_email_for_notifications'; import You_can_add_upto_3_emails from '@salesforce/label/c.You_can_add_upto_3_emails'; import Close from '@salesforce/label/c.Close'; import Proceed from '@salesforce/label/c.Proceed'; import About_Credentials from '@salesforce/label/c.About_Credentials'; import Attention from '@salesforce/label/c.Attention'; import Select_a_Pricebook from '@salesforce/label/c.Select_a_Pricebook'; import Admin_API_access_token from '@salesforce/label/c.Admin_API_access_token'; import Error from '@salesforce/label/c.Error'; import Please_enter_Email from '@salesforce/label/c.Please_enter_Email'; import Please_enter_valid_Email from '@salesforce/label/c.Please_enter_valid_Email'; import Please_enter_Site_URL from '@salesforce/label/c.Please_enter_Site_URL'; import Please_enter_Shopify_Store_URL from '@salesforce/label/c.Please_enter_Shopify_Store_URL'; import Please_enter_ApiAccessToken from '@salesforce/label/c.Please_enter_ApiAccessToken'; import Please_enter_PriceBook from '@salesforce/label/c.Please_enter_PriceBook'; import Email_must_be_less_than_or_equal_to_38_characters from '@salesforce/label/c.Email_must_be_less_than_or_equal_to_38_characters'; import You_cannot_enter_Duplicate_Email from '@salesforce/label/c.You_cannot_enter_Duplicate_Email'; import Please_authorize_Access_Token from '@salesforce/label/c.Please_authorize_Access_Token'; import Please_check_the_credentials_first from '@salesforce/label/c.Please_check_the_credentials_first'; import Please_enter_shopify_store_name from '@salesforce/label/c.Please_enter_shopify_store_name'; import Please_enter_admin_api_access_token from '@salesforce/label/c.Please_enter_admin_api_access_token'; import Store_is_Not_Authorized from '@salesforce/label/c.Store_is_Not_Authorized'; import Please_enter_store_name from '@salesforce/label/c.Please_enter_store_name'; import Please_enter_password from '@salesforce/label/c.Please_enter_password'; import Store_is_Not_having_Access from '@salesforce/label/c.Store_is_Not_having_Access'; import You_do_not_have_access_of_selected_object from '@salesforce/label/c.You_do_not_have_access_of_selected_object'; import Create_Remote_site_Setting from '@salesforce/label/c.Create_Remote_site_Setting'; import Manual_Configuration from '@salesforce/label/c.Manual_Configuration'; import Auto_Configuration from '@salesforce/label/c.Auto_Configuration'; import Check_Credential from '@salesforce/label/c.Check_Credential'; import Generate_Access_Token from '@salesforce/label/c.Generate_Access_Token'; import You_have_changed_the_pricebook from '@salesforce/label/c.You_have_changed_the_pricebook'; import You_have_changed_the_currency from '@salesforce/label/c.You_have_changed_the_currency'; import Select_Site_URL from '@salesforce/label/c.Select_Site_URL'; import Enter_Store_Name from '@salesforce/label/c.Enter_Store_Name'; import Enter_Access_token from '@salesforce/label/c.Enter_Access_token'; import Select_Pricebook from '@salesforce/label/c.Select_Pricebook'; import Automatic_Configuration from '@salesforce/label/c.Automatic_Configuration'; // SHOW TOAST import { ShowToastEvent } from "lightning/platformShowToastEvent"; export default class ShopifyCredentials extends LightningElement { // Custom in-component toast state (replaces ShowToastEvent) — see showNotification. @track toasts = []; _toastSeq = 0; // GLOBAL VARIABLES @api siteurlHelptext = '/lightning/setup/CustomDomain/home'; StoreLogo = StoreLogo; SFLogo = SFLogo; // storeName is bound from home.js via store-name={storeName}. Converted from a // plain @api field to a getter/setter: home.js's own storeName only updates on // Save & Next (see childSaveEvent), so while a user is typing a name for a brand // new store, home.js is still passing down '' on every parent re-render (spinner // ticks, polling, etc.), which was wiping out what was just typed and kept // disableGenerateAccessToken stuck on. Now an empty/falsy value from the parent // is ignored instead of stomping the locally-typed value. Real logic (validation, // save, disable rules) is unchanged. _storeName = ''; @api get storeName() { return this._storeName; } set storeName(value) { if (value) { this._storeName = value; } } @api storeSelectionType; @api showAuthorize = false; @api showCheck = false; @api shopifyCredentialMultiStore = false; @api isAutomatic; //Add a new property @track defaultSiteUrl = ''; // LOCAL VARIABLES @track siteURL = ''; @track storeUrl = ''; @track storeUrlSelected; @track apiAccessToken; @track disableInfo = false; @track priceBook = ''; @track defaultCurrencyType = ''; @track priceBookList = []; @track siteURLList = []; @track configObject = {}; @track batchProcessingObject = {}; addEmailAddress = true; stopEmailError = false; keyIndex = 1; @track checkIndexNo = 0; @track imageChange = true; @track showProcessModel = false; @track deleteEmailIds = ''; @track authorizedStore = false; @track authorizedDone = false; @track storeulgetinfo = storeulgetinfo; @track storepassword = storepassword; @track multiStoreModel = true; @track isChanged = false; @track warringMessage = []; @track oldCurrencyISOCode = ''; @track emaiList = ""; @track Emailvalue = ""; @track currencyISOCode = []; @track currencyCodeList = []; @track currencyCodeListNew = []; @track showCurrencyCode = false; @track storeSelectedCurrency = ""; // DISPLAY-ONLY: server-formatted "Last order synced X ago" text ('' = none). @track _lastSyncedText = ''; _syncTimerId = null; @track storeSelectedCurrencyDefault = ""; @track multiStoreSelectionValue = ''; @track productSyncValue = true; @track customerSyncValue = true; @track collectionSyncValue = true; @track orderSyncValue = true; @track draftSyncValue = true; @track inventorySyncValue = true; @track collectionSyncChanged = false; @track orderSyncChanged = false; @track draftSyncChanged = false; @track inventorySyncChanged = false; @track storedUsername; @track storedPassword; @track addButtonVisible = true; @track storeNameSelected = false; @track accessTokenAvailable = false; @track currencyCodeChanged = false; @track openPricebookCurrencyModal = false; @track oldPricebook = ''; @track newPricebook = ''; @track oldCurrency = ''; @track isPricebookOrCurrencyChanged = false; @track isPricebookChanged = false; @track isCurrencyChanged = false; @track changePricebookMessage = ''; @track changeCurrencyMessage = ''; @track listAccessScopeError = []; @track listShopfyObjectAccessError; @track AccessScope = false; @track isLoading = false; @track isRemoteSiteCreated = false; @track emailListChanged = false; @track checkCredential = false; @track isStoreAuthorized = false; @track isSiteSupportedOrg = true; @track isSitePresentOrg = true; @track currentUserEmail; @track userId = USER_ID; organizationName; // pageLoadingGif = pageLoadingGif; pageLoadingGif = SMEPageLoadingGIF2; @track remoteSiteForStore = false; // Generate Access Token modal state @track showGenerateTokenModal = false; @track generatorClientId = ''; @track generatorClientSecret = ''; @track isGeneratingToken = false; @track clientFieldsReadOnly = true; // readonly-until-focus to defeat browser autofill checkAccessTokenInterval; tokenGenTimeout; // hard cap so the generating spinner never runs forever countdownInterval; // ticks the visible countdown popupWatcher; // polls until the Shopify OAuth popup is closed @track tokenGenCountdown = 30; // seconds remaining before auto-close @track countdownStarted = false; // true only after the OAuth popup has closed syncSpinnerLogo = syncLogo1; // SME logo shown spinning while waiting for the token generateTokenHeaderLogo = SMELOGO; // Sync Made Easy logo shown in the Generate Token modal header //Custom Labels @track label = { Product, Customer, Draft_Order, Order, Collection, Location, Inventory, Generate_Access_Token_Info, Please_enter_your_store_name, Get_your_Shopify_password_from_private_app, THIS_ACTION_REQUIRES_MERCHANT_APPROVAL, Select_a_Site_URL, Salesforce_Sites_enables_you_to_create_public, click_here, Shopify_Store_Name, The_standard_price_book_is_the_master_list, Select_a_Currency, Multicurrency, Default_Currency, Default_Organization_Currency_ISO_Code, Real_time_Sync, Some_objects_are_mandatory, Please_add_email_for_notifications, You_can_add_upto_3_emails, Automatic_Configuration, Close, About_Credentials, Attention, Select_a_Pricebook, Admin_API_access_token, Error, Please_enter_Email, Please_enter_valid_Email, Please_enter_Site_URL, Please_enter_Shopify_Store_URL, Please_enter_ApiAccessToken, Please_enter_PriceBook, Email_must_be_less_than_or_equal_to_38_characters, You_cannot_enter_Duplicate_Email, Please_authorize_Access_Token, Please_check_the_credentials_first, Please_enter_shopify_store_name, Please_enter_admin_api_access_token, Store_is_Not_Authorized, Please_enter_store_name, Please_enter_password, Store_is_Not_having_Access, You_do_not_have_access_of_selected_object, Create_Remote_site_Setting, Auto_Configuration, Manual_Configuration, Check_Credential, Generate_Access_Token, Proceed, You_have_changed_the_pricebook, You_have_changed_the_currency, Select_Site_URL, Enter_Store_Name, Enter_Access_token, Select_Pricebook }; //Email List to add emails entered by the user @track emailList = [{ index: this.keyIndex, value: '', addEmail: this.addEmailAddress, exists: false, isEditable: true, isValid: false, isDeleteDisabled: true }]; @wire(getRecord, { recordId: '$userId', fields: [USER_EMAIL] }) currentUserInfo({ error, data }) { if (data) { this.currentUserEmail = data.fields.Email.value; console.log('>>> This is the current user email :: ' + this.currentUserEmail); } else if (error) { console.log('>>> Error in email fetching ' + JSON.stringify(error)); // this.error = error; } } changeConfigurationType() { this.isAutomatic = !this.isAutomatic; this.dispatchEvent(new CustomEvent('automation', { detail: this.isAutomatic })); } //Connected Callback connectedCallback() { console.log('SYNC MADE EASY SME: Credential Connected Callback' + this.isStoreAuthorized); this.showSpinnerHandler(); if (this.storeName && !this.shopifyCredentialMultiStore) { this.multiStoreModel = false; this.storeNameSelected = true; this.accessTokenAvailable = true; this.storeSelectionType = 'Existing Store'; this.getconfigrationForDynamicRow(); } else if (!this.storeName) { this.getStores(); } this.getActiveCurrenciesList(); this.getActivePricebookList(); this.showAuthorize = false; this.getErrorMailListMethod(); this.fetchDefaultSiteUrl(); this.loadLastOrderSyncTime(); // Refresh the server-computed "X ago" label every 60s. this._syncTimerId = setInterval(() => { this.loadLastOrderSyncTime(); }, 60000); } /** * This Method is used to get all the stores which are present in org. * */ getStores() { console.log('SYNC MADE EASY: Credential => Getting List of Stores'); this.multiStoreModel = false; getConfigrations().then((result) => { if (result) { this.storeList = JSON.parse(result); if (!this.storeList || this.storeList.length === 0) { this.multiStoreModel = false; this.multiStoreHandlerEvent('New'); } else if (this.storeList && this.storeList.length === 1) { this.storeName = this.storeList[0].Name; this.multiStoreHandlerEvent('Existing Store'); } else { this.multiStoreModel = true; } } else { this.multiStoreModel = false; this.multiStoreHandlerEvent('New'); } }).catch((error) => { //Error occured this.closeSpinnerHandler(); }); } //This method will get all the Currency ISO getActiveCurrenciesList() { console.log('SYNC MADE EASY: FETCH CURRENCIES'); getActiveCurrencies().then((result) => { this.storeSelectedCurrencyDefault = result[0]; if (result[1] != null && result[1] != '') { this.currencyCodeList = JSON.parse(result[1]).map(activecurrency => { return ({ label: activecurrency.ISOCode, value: activecurrency.ISOCode }) }); this.showCurrencyCode = true; } this.handleDefaultSelectedCurrency(); }).catch((error) => { }); } //Getting store records getconfigrationForDynamicRow() { console.log('SYNC MADE EASY: Credential => Fetching Store Data ' + this.storeName); getConfigrationForDynamicRow({ store: this.storeName }) .then((result) => { this.configObject = JSON.parse(result); if (this.configObject.CurrencyIsoCode) { this.storeSelectedCurrency = this.configObject.CurrencyIsoCode; this.handleDefaultSelectedCurrency(); } this.productSyncValue = this.configObject.HIC_ShopifySync__Product_Sync__c; this.customerSyncValue = this.configObject.HIC_ShopifySync__Customer_Sync__c; this.collectionSyncValue = this.configObject.HIC_ShopifySync__Collection_Sync__c; this.orderSyncValue = this.configObject.HIC_ShopifySync__Order_Sync__c; this.draftSyncValue = this.configObject.HIC_ShopifySync__Draft_Order_Sync__c; this.inventorySyncValue = this.configObject.HIC_ShopifySync__Inventory_Location__c; this.siteURL = this.defaultSiteUrl || this.configObject.HIC_ShopifySync__Salesforce_Site_URL__c; // To this — keep the default always: this.siteURL = this.defaultSiteUrl || this.siteURL; this.storeUrl = this.configObject.HIC_ShopifySync__Shopify_URL__c; this.apiAccessToken = this.configObject.HIC_ShopifySync__Private_Key_Password__c; this.priceBook = this.configObject.HIC_ShopifySync__Salesforce_Pricebook__c; if (this.priceBookList.length > 0) { let isPriceBookExists = false; for (let price of this.priceBookList) { if (this.priceBook === price.value) { isPriceBookExists = true; } } if (!isPriceBookExists) { this.isPricebookChanged = true; this.priceBook = this.priceBookList[0].value; } } if (this.configObject.HIC_ShopifySync__Product_Sync__c == null) { this.configObject.HIC_ShopifySync__Product_Sync__c = false; } if (this.isSiteSupportedOrg && this.storeSelectionType === 'Existing Store') { // Checking Remote site Setting for Existing Store this.createStoreUrlRemoteSite(this.storeUrl, this.apiAccessToken); } else if (!this.isSiteSupportedOrg && this.storeSelectionType === 'Existing Store') { this.checkStoreNameRemoteSiteSetting(); } this.closeSpinnerHandler(); }).catch((error) => { this.closeSpinnerHandler(); }) } handleDefaultSelectedCurrency() { let currencySelectedExists = false; let currencyDefaultExists = false; if (this.currencyCodeList.length > 0 && this.storeSelectedCurrencyDefault != '') { for (let currencyCode of this.currencyCodeList) { if (this.storeSelectedCurrency === currencyCode.value) { currencySelectedExists = true; } if (this.storeSelectedCurrencyDefault === currencyCode.value) { currencyDefaultExists = true; } } if (currencySelectedExists == false && currencyDefaultExists == true) { this.isCurrencyChanged = true; this.storeSelectedCurrency = this.storeSelectedCurrencyDefault; } else if (currencySelectedExists == false && currencyDefaultExists == false) { this.isCurrencyChanged = true; this.storeSelectedCurrency = this.currencyCodeList[0].value; } } else if (this.storeSelectedCurrencyDefault != '') { this.isCurrencyChanged = true; this.storeSelectedCurrency = this.storeSelectedCurrencyDefault; } } //This method saves the true/false value of Product toggle changeProductSync(event) { this.productSyncValue = event.target.checked; } //his method saves the true/false value of Customer toggle changeCustomerSync(event) { this.customerSyncValue = event.target.checked; } //This method saves the true/false value of Collection toggle changeCollectionSync(event) { this.collectionSyncValue = event.target.checked; if (this.configObject.HIC_ShopifySync__Collection_Sync__c !== this.collectionSyncValue) { this.collectionSyncChanged = true; } else { this.collectionSyncChanged = false; } this.handleAccessScope(); } //This method saves the true/false value of Order toggle changeOrderSync(event) { this.orderSyncValue = event.target.checked; if (this.configObject.HIC_ShopifySync__Order_Sync__c !== this.orderSyncValue) { this.orderSyncChanged = true; } else { this.orderSyncChanged = false; } this.handleAccessScope(); } //This method saves the true/false value of Draft toggle changeDraftSync(event) { const newValue = event.target.checked; const oldValue = this.draftSyncValue; this.draftSyncValue = newValue; this.draftSyncChanged = this.configObject.HIC_ShopifySync__Draft_Order_Sync__c !== newValue; if (!oldValue && newValue) { this.configObject.HIC_ShopifySync__Enable_Draft_Order__c = true; this.configObject.HIC_ShopifySync__Enable_Opportunity_for_SDO__c = true; } if (oldValue && !newValue) { this.configObject.HIC_ShopifySync__Enable_Draft_Order__c = false; this.configObject.HIC_ShopifySync__Enable_Opportunity_for_SDO__c = false; } this.previousDraftSyncValue = newValue; this.handleAccessScope(); } //This method saves the true/false value of Inventory toggle changeInventorySync(event) { this.inventorySyncValue = event.target.checked; if (this.configObject.HIC_ShopifySync__Inventory_Location__c !== this.inventorySyncValue) { this.inventorySyncChanged = true; } else { this.inventorySyncChanged = false; } this.handleAccessScope(); } //This method controls the closing of multistore modal closeMultiStoreModal(event) { if (event.detail == 'closeModal') { this.multiStoreModel = false; } } //This method is getting the store value // getStore() { // console.log('SYNC MADE EASY: Credential => Fetching Configuration'); // getConfigration().then((result) => { // this.configObject = JSON.parse(result); // this.siteURL = this.configObject.HIC_ShopifySync__Salesforce_Site_URL__c; // this.storeUrl = this.configObject.HIC_ShopifySync__Shopify_URL__c; // this.apiAccessToken = this.configObject.HIC_ShopifySync__Private_Key_Password__c; // this.priceBook = this.configObject.HIC_ShopifySync__Salesforce_Pricebook__c; // }).catch((error) => { // //Error occured // this.closeSpinnerHandler(); // }); // } //This method is getting the site URL getSiteUrlList() { console.log('SYNC MADE EASY: Credential => Fetching Sites'); return getSiteURLs().then((result) => { if (result != null) { if (result == 'No access to site.') { this.closeSpinnerHandler(); this.isSiteSupportedOrg = false; this.isSitePresentOrg = false; console.log('checking..'); } else { // this.isSitePresentOrg = true; this.siteURLList = []; for (let url of JSON.parse(result)) { this.siteURLList.push({ label: url.siteUrl, value: url.siteUrl }); } this.closeSpinnerHandler(); } } }).catch((error) => { //Error occured this.closeSpinnerHandler(); }); } // his method is getting all the Pricebooks getActivePricebookList() { console.log('SYNC MADE EASY: Credential => Fetching Pricebooks'); getActivePricebook().then((result) => { //console.log('SYNC MADE EASY: Credential => Fetching Pricebooks : '+result); this.priceBookList = []; for (let pricebook of JSON.parse(result)) { this.priceBookList.push({ label: pricebook.Name, value: pricebook.Id, IsStandard: pricebook.IsStandard }); } if (this.priceBook) { if (this.priceBookList) { let isPriceBookExists = false; for (let price of this.priceBookList) { if (this.priceBook === price.value) { isPriceBookExists = true; } } if (!isPriceBookExists) { this.isPricebookChanged = true; this.priceBook = this.priceBookList[0].value; } } } this.closeSpinnerHandler(); }).catch((error) => { this.closeSpinnerHandler(); }); } //This method is handling Email Add button and adding the emails to the list addEmail(event) { const emailRegex = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/; if (this.emailList.length < 3) { if (this.emailList[this.checkIndexNo].value.match(emailRegex)) { let indexNumber = ++this.keyIndex; this.addEmailAddress = false; let newItem = { index: indexNumber, value: '', addEmail: this.addEmailAddress, exists: false, isEditable: true, isValid: false }; this.emailList = [...this.emailList, newItem]; ++this.checkIndexNo; } else if (this.emailList[this.checkIndexNo].value === '') { this.showNotification(this.label.Error, 'Error', this.label.Please_enter_Email); } else { this.showNotification(this.label.Error, 'Error', this.label.Please_enter_valid_Email); } } if (this.emailList.length > 2) { this.addButtonVisible = false; } this.updateDeleteState(); } //This method is handling deleteting emails from the email list deleteEmail(event) { this.emailListChanged = true; if (this.checkIndexNo <= 0) { this.checkIndexNo = 1; } this.addButtonVisible = true; if (event.target.dataset.id) { this.deleteEmailIds = this.deleteEmailIds + ',' + event.target.dataset.id; } let indexNumber = 0; let updatedEmailList = this.emailList.map((email) => { if (email.index != event.target.dataset.id) { email.index = ++indexNumber; return email; } else { return null; } }); updatedEmailList = updatedEmailList.filter((obj) => obj !== null); this.emailList = updatedEmailList; this.keyIndex--; this.checkIndexNo--; this.updateDeleteState(); } get showDelete() { return this.emailList.length > 1; } //This method is getting list of Emails to send email notifications after Batch processing is completed getErrorMailListMethod() { getErrorMailList() .then((result) => { if (result) { this.emailList = []; let eIndex = 0; for (let email of JSON.parse(result)) { ++eIndex; this.emailList.push({ index: eIndex, value: email.Name, addEmail: this.addEmailAddress, exists: false, isEditable: false, isValid: true }); this.addEmailAddress = false; if (this.emailList.length > 2) { this.addButtonVisible = false; } } this.keyIndex = eIndex; this.updateDeleteState(); } this.closeSpinnerHandler(); }).catch((error) => { //Error occured this.closeSpinnerHandler(); }) } //This method is handling the emails entered/updated from the UI handleEmailValue(event) { this.emailListChanged = true; const emailRegex = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/; for (let email of this.emailList) { if (email.index == event.target.dataset.id) { email.value = event.target.value; email.isValid = emailRegex.test(email.value); } } } //??? Check the functionality, not calling anywhere OpenSliderPopupFirst() { this.showProcessModel = true; this.imageChange = true; } //This method is used to show helptext to get access token OpenSliderPopupSecond() { this.showProcessModel = true; this.imageChange = false; } //This method handles closing of 'About Credentials' modal closeWarningModal(event) { this.showProcessModel = false; } // handleNext(event) { this.imageChange = false; } handleBack(event) { this.imageChange = true; } handleFinish(event) { this.showProcessModel = false; } //This method handles the type of store selected multiStoreHandler(event) { let subject = event.detail.subject; this.storeSelectionType = event.detail.subject; this.storeName = event.detail.storeName; this.multiStoreHandlerEvent(subject); this.loadLastOrderSyncTime(); } //This method handles the functionality based on the type of store selected multiStoreHandlerEvent(subject) { if (subject == 'Edit') { this.getconfigrationForDynamicRow(); this.showAuthorize = true; this.multiStoreModel = false; this.storeNameSelected = false; this.accessTokenAvailable = false; this.storeSelectionType = subject; this.checkCredential = true; this.handlestoreselection(false); } else if (subject == 'Existing Store') { this.showSpinnerHandler(); this.getconfigrationForDynamicRow(); this.multiStoreModel = false; this.showAuthorize = false; this.storeNameSelected = true; this.accessTokenAvailable = true; this.storeSelectionType = subject; this.checkCredential = false; this.handlestoreselection(true); } else if (subject == 'New') { // if (this.isSiteSupportedOrg && this.siteURLList != null && this.siteURLList.length > 0) { // this.siteURL = this.siteURLList[0].value; // } this.storeUrl = ''; this.apiAccessToken = ''; // Clear via the backing field directly: the storeName setter now ignores // empty values (so parent re-renders can't stomp on what's being typed), // but starting a brand new store must still wipe any leftover name. this._storeName = ''; this.showAuthorize = true; if (this.priceBookList) { for (let price of this.priceBookList) { if (price.IsStandard) { this.priceBook = price.value; } } if (!this.priceBook) { this.priceBook = this.priceBookList[0].value; } } this.storeSelectedCurrency = this.storeSelectedCurrencyDefault; this.multiStoreModel = false; this.storeNameSelected = false; this.accessTokenAvailable = true; this.storeSelectionType = subject; this.checkCredential = true; this.handlestoreselection(false); } else if (subject == 'Delete') { this.storeUrl = ''; this.apiAccessToken = ''; this._storeName = ''; this.showAuthorize = false; this.multiStoreModel = true; this.storeNameSelected = false; this.accessTokenAvailable = true; } } // handle multistore selection, edit or new handlestoreselection(isExistingStore) { const customEvent = new CustomEvent('existingmultistore', { detail: { subject: isExistingStore, storeName: this.storeName, storeSelectType: this.storeSelectionType } }); this.dispatchEvent(customEvent); } // Save button on Shopify Credentials page @api async handleSaveShopifyCredentials() { this.openPricebookCurrencyModal = this.isPricebookOrCurrencyChanged; if (!this.openPricebookCurrencyModal) { this.showSpinnerHandler(); this.saveShopifyCredentials(); } } saveShopifyCredentials() { // Show spinner while it is going to the next page this.showSpinnerHandler(); let emailsValid = this.validateEmails(); console.log('>>> emailsValid =>' + emailsValid); if (!emailsValid) { return null; } console.log('>>> this.siteURL ' + this.siteURL); console.log('>>> this.isSitePresentOrg ' + this.isSitePresentOrg); console.log('>>> this.storeUrl ' + this.storeUrl); console.log('>>> this.apiAccessToken ' + this.apiAccessToken); console.log('>>> this.priceBook ' + this.priceBook); if ((this.siteURL == null || this.siteURL.length == 0) && this.siteURLList != null && this.siteURLList.length > 0) { this.siteURL = this.siteURLList[0].value; console.log('Site url Fixed :: ' + this.siteURL); } //Validations for Input fields : Site URL, Shopify URL, Access Token, Pricebook if (this.isSitePresentOrg && (this.siteURL == null || this.siteURL.length == 0)) { console.log('>>> Professional Edition'); this.showNotification(this.label.Error, 'Error', this.label.Please_enter_Site_URL); this.checkStatusSpinner = false; this.closeSpinnerHandler(); } else if (this.storeUrl == null || this.storeUrl == '') { this.showNotification(this.label.Error, 'Error', this.label.Please_enter_Shopify_Store_URL); } else if (this.apiAccessToken == null || this.apiAccessToken == '') { this.showNotification(this.label.Error, 'Error', this.label.Please_enter_ApiAccessToken); } else if (this.priceBook == null || this.priceBook == '') { this.showNotification(this.label.Error, 'Error', this.label.Please_enter_PriceBook); } console.log('>>> this.siteURL 1 ' + this.siteURL); console.log('>>> this.isSitePresentOrg ' + this.isSitePresentOrg); console.log('>>> this.storeUrl ' + this.storeUrl); console.log('>>> this.apiAccessToken ' + this.apiAccessToken); console.log('>>> this.priceBook ' + this.priceBook); if (((this.siteURL != null && this.siteURL != '') || !this.isSitePresentOrg) && this.storeUrl != null && this.storeUrl != '' && this.apiAccessToken != null && this.apiAccessToken != '' && this.priceBook != null && this.priceBook != '') { console.log('@@@@ THIS IS PRCEED AUTHORIZATION'); this.proceedAuthorization(this.storeUrl, this.apiAccessToken); let storename = this.configObject.HIC_ShopifySync__Shopify_URL__c.replaceAll('https://', '').replace('.myshopify.com', '').replace('/', ''); this.configObject.Name = storename; this.configObject.HIC_ShopifySync__Store__c = storename; } } //METHOD TO Validate Email validateEmails() { const emailRegex = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/; // Check if any email field is blank for (let email of this.emailList) { console.log('>>> THIS IS EMAIL LENGTH : ' + email.value.length); if (!email.value.trim()) { this.showNotification(this.label.Error, 'Error', this.label.Please_enter_Email); return false; } else if (email.value.length >= 39) { this.showNotification(this.label.Error, 'Error', this.label.Email_must_be_less_than_or_equal_to_38_characters); return false; } } if (this.emailList.length < 1 && this.isAutomatic) { this.emailList.push(this.currentUserEmail); console.log('>>> Validated User Email in Automatic Mode :: ' + JSON.stringify(this.emailList)); } if (this.emailList.length > 0) { let emailString = []; for (let i = this.emailList.length - 1; i >= 0; i--) { if (this.emailList[0].value === '') { this.addButtonVisible = true; this.showNotification(this.label.Error, 'Error', this.label.Please_enter_Email); return false; } else if (!this.emailList[i].value.match(emailRegex) && this.emailList[i].value != '') { this.showNotification(this.label.Error, 'Error', this.label.Please_enter_valid_Email); return false; } else { if (this.emailList[i].value === null || this.emailList[i].value === '') { this.emailList.splice(i, 1); } else if (!this.emailList[i].value.match(emailRegex)) { this.showNotification(this.label.Error, 'Error', this.label.Please_enter_valid_Email); return false; } // CHECK THE DUPLICATE EMAIL else if (this.emailList[i].exists == true) { this.showNotification(this.label.Error, 'Error', this.label.You_cannot_enter_Duplicate_Email); return false; } } } emailString = this.emailList.map((email) => { return { 'Name': email.value, 'HIC_ShopifySync__Email_Id__c': email.value }; }); if (emailString.length >= 0 && this.emailListChanged) { saveEmailSettings({ emaiList: JSON.stringify(emailString) }).then((result) => { //Success }).catch((error) => { //Error occured this.closeSpinnerHandler(); }); } // THIS NEED A LOOK AGAIN NOT USING AUTHORIZE BUTTON if ((this.storeSelectionType === 'New' && this.showAuthorize) || this.showCheck || !this.apiAccessToken || (!this.isSiteSupportedOrg && this.remoteSiteForStore)) { this.showNotification(this.label.Error, 'Error', this.label.Please_authorize_Access_Token); return; } } return true; } //Value stored on onchange Store URL changeStoreUrl(event) { const typedValue = event.target.value.trim(); if (typedValue === '') { // Explicit user clear — write the backing field directly so the // storeName setter's "ignore falsy" guard (meant only to block the // parent from stomping a typed value on re-render) doesn't also // swallow this intentional clear and leave the old name stuck. this._storeName = ''; this.storeUrl = null; } else { this.storeName = typedValue; if (this.storeName.includes("https://") || this.storeName.includes(".myshopify.com") || this.storeName.includes('http://')) { this.storeUrl = this.storeName; this._storeName = this.storeName.replace('https://', '').replace('http://', '').replace('.myshopify.com', ''); } else { this.storeUrl = "https://" + this.storeName + ".myshopify.com"; } } if (this.storeName) { this.accessTokenAvailable = false; } else { this.accessTokenAvailable = true; } } //Value stored on onchange Access Token changeAccessToken(event) { this.apiAccessToken = event.target.value; } //Value stored on onchange ISO Currency Code // METHOD TO HANDLE CURRENCY CHANGE MESSAGE changeCurrencyISOCode(event) { this.storeSelectedCurrency = event.target.value; this.currencyCodeChanged = true; if (this.storeSelectionType == 'New') { this.oldCurrency = this.storeSelectedCurrencyDefault; } else { this.oldCurrency = this.configObject.CurrencyIsoCode; } this.changeCurrencyMessage = ''; if ((this.storeSelectedCurrency != this.oldCurrency)) { this.isCurrencyChanged = true; this.isPricebookOrCurrencyChanged = true; this.changeCurrencyMessage = `${this.label.You_have_changed_the_currency} ${this.oldCurrency} to ${this.storeSelectedCurrency}`; } else { this.isCurrencyChanged = false; this.isPricebookOrCurrencyChanged = false; } } // This Method is for Changing Site URL changeSiteUrl(event) { this.siteURL = event.detail.value; } // METHOD TO HANDLE THE PRICEBOOK CHANGE MESSAGE changePriceBook(event) { this.priceBook = event.detail.value; let oldPricebookValues = []; let newPricebookValues = []; let showpricebooklist = this.priceBookList; oldPricebookValues = showpricebooklist.map((obj) => { if (this.configObject.HIC_ShopifySync__Salesforce_Pricebook__c && obj.value == this.configObject.HIC_ShopifySync__Salesforce_Pricebook__c) { this.oldPricebook = obj.label; } else if (!this.configObject.HIC_ShopifySync__Salesforce_Pricebook__c) { this.oldPricebook = this.priceBookList[0].label; } }); newPricebookValues = showpricebooklist.map((obj1) => { if (obj1.value == this.priceBook) { this.newPricebook = obj1.label; } }); this.changePricebookMessage = ''; if (this.newPricebook != this.oldPricebook) { this.isPricebookChanged = true; this.isPricebookOrCurrencyChanged = true; this.changePricebookMessage = `${this.label.You_have_changed_the_pricebook} ${this.oldPricebook} to ${this.newPricebook}`; } else { this.isPricebookChanged = false; this.isPricebookOrCurrencyChanged = false; this.changePricebookMessage = ''; } } /** * This Method is used to close the Modal of pricebook and currency change warning */ closePricebookCurrencyModal(event) { this.openPricebookCurrencyModal = false; } //This method is used to permit to configure the values if each of them is not null proceedAuthorization(shopifyUrl, PASSWORD) { console.log('>>> proceedAuthorization'); if ((!this.showAuthorize && this.isSiteSupportedOrg) || (!this.isSiteSupportedOrg && !this.remoteSiteForStore)) { this.saveConfigrationFunction(); } else { this.showNotification(this.label.Error, 'Error', Please_check_the_credentials_first); } } //Creating remote site when credentials are valid and store is existing store createStoreUrlRemoteSite(shopifyUrl, PASSWORD) { this.showSpinnerHandler(); console.log('SYNC MADE EASY: Credential => Creating Remote site for: ' + shopifyUrl); createRemoteSiteSettings({ 'URL': shopifyUrl }).then((result) => { console.log('SYNC MADE EASY: Credential => Remote site result: ' + result); if (result === 'Success' || result === 'Remote Site setting') { console.log('SYNC MADE EASY: Credential => Remote site OK, storeSelectionType: ' + this.storeSelectionType + ', isStoreAuthorized: ' + this.isStoreAuthorized); // this.validCreds = true; if (this.storeSelectionType !== 'Existing Store' && !this.isStoreAuthorized) { console.log('SYNC MADE EASY: Credential => Calling checkCredentials'); this.checkCredentials(shopifyUrl, PASSWORD); } else { console.log('SYNC MADE EASY: Credential => Calling validateCredentials'); this.validateCredentials(); } this.isRemoteSiteCreated = true; } else { console.log('SYNC MADE EASY: Credential => Remote site FAILED - spinner will be stuck here'); this.isRemoteSiteCreated = false; } }).catch((error) => { console.log('SYNC MADE EASY: Credential => Remote site catch error: ' + JSON.stringify(error)); this.closeSpinnerHandler(); }); } // Opens the Generate Token modal (validates store name first) handleGenerateAccessToken() { if (!this.storeName) { this.showNotification('Error', 'Error', 'Please enter Shopify store name first'); return; } this.generatorClientId = ''; this.generatorClientSecret = ''; this.clientFieldsReadOnly = true; // keep readonly so the browser won't autofill this.showGenerateTokenModal = true; this.lockBackgroundScroll(); // Make sure the modal opens with its header visible instead of // wherever the overlay's scroll happened to land (see the // .sw-modal-overlay overflow-y fix in the CSS for why the overlay // needs its own scroll at all). requestAnimationFrame(() => { const overlay = this.refs.genModalOverlay; if (overlay) { overlay.scrollTop = 0; } }); } // Unlock the credential fields only once the user actively focuses them. // Chrome/Edge skip readonly inputs during their autofill pass, so the fields stay empty. handleCredentialFocus() { this.clientFieldsReadOnly = false; } closeGenerateTokenModal() { this.showGenerateTokenModal = false; this.generatorClientId = ''; this.generatorClientSecret = ''; this.clientFieldsReadOnly = true; // Stop any in-progress generation/polling when the user cancels/closes this.isGeneratingToken = false; this.countdownStarted = false; this.clearTokenGenTimers(); this.unlockBackgroundScroll(); } // NOTE: an earlier version of this fix moved the modal's DOM node to // document.body ("portalling") to escape a suspected transformed // ancestor breaking position:fixed. That was reverted — it caused a // worse regression (Client ID / Client Secret stopped registering what // was typed, since LWC's re-render reconciliation doesn't expect its // own template nodes to have been moved externally). The centering/ // zoom-level issue may still need a proper fix (e.g. migrating this // modal to the official lightning/modal base component), but a broken // form is worse than an imperfectly-centered one, so this was rolled // back to keep the modal in its normal place in the template. // Presentation-only helpers: the modal overlay is position:fixed and covers // the viewport, but mouse-wheel/touch scroll can still scroll the page // behind it unless the body itself is locked. Reused by any modal that // needs the same "background frozen while open" behavior. lockBackgroundScroll() { if (typeof document !== 'undefined' && document.body) { document.body.style.overflow = 'hidden'; } } unlockBackgroundScroll() { if (typeof document !== 'undefined' && document.body) { document.body.style.overflow = ''; } } // Generate Token button stays blurred/disabled until BOTH Client ID and // Client Secret have something typed in — matches the "blur until // filled, then enable" requirement. get disableGenerateTokenSubmit() { const clientIdEmpty = !this.generatorClientId || !this.generatorClientId.trim(); const clientSecretEmpty = !this.generatorClientSecret || !this.generatorClientSecret.trim(); return clientIdEmpty || clientSecretEmpty; } get generateTokenSubmitClass() { return this.disableGenerateTokenSubmit ? 'btn-success gen-token-submit-btn gen-token-submit-btn_disabled' : 'btn-success gen-token-submit-btn'; } handleGeneratorClientIdChange(event) { this.generatorClientId = event.target.value; } handleGeneratorClientSecretChange(event) { this.generatorClientSecret = event.target.value; } // Redirect URI the user must register in their Shopify app. // Uses the @RestResource REST endpoint — works in every subscriber org // without any manual Site page configuration. get generatorRedirectUri() { let base = this.siteURL || ''; if (base.endsWith('/')) base = base.slice(0, -1); return base ? base + '/services/apexrest/HIC_ShopifySync/AuthenticateAllApexClass' : '(Set a Site URL above first)'; } // Full Shopify store URL shown (and copyable) in the Generate Token banner. // Strips any protocol/suffix the user may have typed so it never doubles up. get storeShopifyUrl() { if (!this.storeName) { return ''; } const name = this.storeName .replace('https://', '') .replace('http://', '') .replace('.myshopify.com', '') .replace('/', ''); return 'https://' + name + '.myshopify.com'; } // Copies the full Shopify store URL from the Generate Token banner copyStoreUrl() { const url = this.storeShopifyUrl; if (!url) { this.showNotification('Error', 'Error', 'Please enter a store name first'); return; } if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(url) .then(() => this.showNotification('Success', 'Success', 'Store URL copied to clipboard')) .catch(() => this.fallbackCopy(url, 'Store URL')); } else { this.fallbackCopy(url, 'Store URL'); } } copyRedirectUri() { const uri = this.generatorRedirectUri; if (!uri || uri.indexOf('http') !== 0) { this.showNotification('Error', 'Error', 'Please select a Site URL first'); return; } // Modern clipboard API works only in a secure context; falls back to execCommand // because the Salesforce Lightning iframe often blocks navigator.clipboard. if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(uri) .then(() => this.showNotification('Success', 'Success', 'Redirect URI copied to clipboard')) .catch(() => this.fallbackCopy(uri)); } else { this.fallbackCopy(uri); } } // Fallback copy using a temporary textarea + execCommand (works inside the LWC iframe) fallbackCopy(text, label) { const what = label || 'Redirect URI'; try { const ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', ''); ta.style.position = 'fixed'; ta.style.top = '0'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.focus(); ta.select(); const ok = document.execCommand('copy'); document.body.removeChild(ta); if (ok) { this.showNotification('Success', 'Success', what + ' copied to clipboard'); } else { this.showNotification('Error', 'Error', 'Could not copy automatically. Please copy it manually.'); } } catch (e) { this.showNotification('Error', 'Error', 'Could not copy automatically. Please copy it manually.'); } } // Called when user clicks "Generate Token" inside the modal async handleGenerateTokenSubmit() { // Re-sync from the actual DOM value right before validating: the fields // were reported as showing "required" errors even with visible text // typed in, which means this.generatorClientId/Secret can get out of // sync with what's really in the box (e.g. autofill doesn't always // fire a normal input event). Reading the live value here guarantees // whatever the user can see typed is exactly what gets validated and // sent, regardless of that gap. if (this.refs.clientIdInput) { this.generatorClientId = this.refs.clientIdInput.value; } if (this.refs.clientSecretInput) { this.generatorClientSecret = this.refs.clientSecretInput.value; } const clientIdEmpty = !this.generatorClientId || !this.generatorClientId.trim(); const clientSecretEmpty = !this.generatorClientSecret || !this.generatorClientSecret.trim(); if (clientIdEmpty && clientSecretEmpty) { this.showNotification('Error', 'Error', 'Client ID and Client Secret are required. Please fill out both fields.'); return; } if (clientIdEmpty) { this.showNotification('Error', 'Error', 'Please enter the Client ID'); return; } if (clientSecretEmpty) { this.showNotification('Error', 'Error', 'Please enter the Client Secret'); return; } const redirectUri = this.generatorRedirectUri; if (!this.siteURL) { this.showNotification('Error', 'Error', 'Please select a Site URL before generating a token'); return; } this.isGeneratingToken = true; try { // Ensure a Remote Site Setting exists for the Shopify store — the OAuth // callback page needs it to make the server-side token-exchange callout. const shopifyUrl = 'https://' + this.storeName + '.myshopify.com'; await createRemoteSiteSettings({ URL: shopifyUrl }); // Verify it actually persisted; otherwise the callback callout will be // blocked as "Unauthorized endpoint". Fail fast with a clear message. const remoteSiteReady = await checkStoreNameRemoteSite({ shopifyStoreUrl: shopifyUrl }); if (!remoteSiteReady) { this.isGeneratingToken = false; this.showNotification( 'Error', 'Error', 'Could not authorize the store endpoint automatically. Please add a Remote Site Setting for ' + shopifyUrl + ' (Setup → Remote Site Settings), then try Generate Token again.' ); return; } // Apex builds the auth URL and stores credentials server-side const authUrl = await initShopifyTokenGeneration({ storeName: this.storeName, clientId: this.generatorClientId.trim(), clientSecret: this.generatorClientSecret.trim(), redirectUri: redirectUri }); // Open Shopify OAuth in a popup, centered on the CURRENT browser window // (dual-monitor aware — screen.width alone lands off-center on secondary displays). // IMPORTANT: the window name must be UNIQUE per open — browsers reuse the // geometry of an existing same-named window and ignore top/left/width/height, // which made the popup reopen half-screen/docked after being snapped once. const width = 800, height = 700; const dualLeft = window.screenLeft !== undefined ? window.screenLeft : (window.screenX || 0); const dualTop = window.screenTop !== undefined ? window.screenTop : (window.screenY || 0); const winWidth = window.outerWidth || window.innerWidth || screen.availWidth; const winHeight = window.outerHeight || window.innerHeight || screen.availHeight; const left = Math.max(0, Math.round(dualLeft + (winWidth - width) / 2)); const top = Math.max(0, Math.round(dualTop + (winHeight - height) / 2)); const popup = window.open( authUrl, 'ShopifyOAuth_' + Date.now(), `popup=yes,width=${width},height=${height},top=${top},left=${left},toolbar=no,menubar=no,scrollbars=yes,resizable=yes` ); // Reset any prior timers/watchers. this.clearTokenGenTimers(); this.countdownStarted = false; // Watch the Shopify popup. The token is DISPLAYED in that window (it is // never saved by the callback page — no guest-user DML). Once the user // copies it and closes the popup, prompt them to paste it manually. let waited = 0; this.popupWatcher = setInterval(() => { waited += 500; if (!popup || popup.closed) { clearInterval(this.popupWatcher); this.finishTokenGeneration(); } else if (waited >= 300000) { // Safety net: popup left open for 5 minutes — give up. clearInterval(this.popupWatcher); if (this.isGeneratingToken) { this.showNotification('Error', 'Error', 'Authorization was not completed. Please try again.'); this.closeGenerateTokenModal(); } } }, 500); } catch (error) { console.error('Generate token error:', error); const msg = (error.body && error.body.message) ? error.body.message : 'Failed to initiate token generation'; this.showNotification('Error', 'Error', msg); this.isGeneratingToken = false; } } // Called once the Shopify OAuth popup has closed. The token was displayed in // the popup for the user to copy — close the modal and prompt them to paste // it into the Admin API access token field, then run Check Credential. finishTokenGeneration() { if (!this.isGeneratingToken) { return; } this.isGeneratingToken = false; this.countdownStarted = false; this.clearTokenGenTimers(); this.showGenerateTokenModal = false; this.generatorClientId = ''; this.generatorClientSecret = ''; this.clientFieldsReadOnly = true; this.showNotification('Success', 'Success', 'Paste the copied access token into the "Admin API access token" field, then click "Check Credential".'); } // Clears every timer/interval/watcher used by the token generation flow. clearTokenGenTimers() { if (this.checkAccessTokenInterval) { clearInterval(this.checkAccessTokenInterval); } if (this.countdownInterval) { clearInterval(this.countdownInterval); } if (this.tokenGenTimeout) { clearTimeout(this.tokenGenTimeout); } if (this.popupWatcher) { clearInterval(this.popupWatcher); } } // Handling button 'Check Credentials' checkStoreCredentials() { console.log('this.siteURL-->>', this.siteURL); console.log('this.storeName-->>', this.storeName); console.log('this.storeUrl-->>', this.storeUrl); if (!this.storeName && !this.storeUrl) { this.showNotification(this.label.Error, 'Error', this.label.Please_enter_shopify_store_name); } else if (this.isSiteSupportedOrg) { this.createStoreUrlRemoteSite(this.storeUrl, this.apiAccessToken); } else { this.checkStoreNameRemoteSiteSetting(); } } //VALIDATE CREDENTIALS WHEN STORE IS EDIT OR NEW validateCredentials() { if (this.storeUrl && this.apiAccessToken) { console.log('SYNC MADE EASY: Credential => Checking Credentials'); checkCreds({ 'shopifyUrl': this.storeUrl, 'PASSWORD': this.apiAccessToken }).then((result) => { if (result == true) { this.accessTokenAvailable = true; this.storeNameSelected = true; this.isStoreAuthorized = true; this.showAuthorize = false; this.checkAccessScope(this.storeUrl, this.apiAccessToken); } else { this.showNotification(this.label.Error, 'Error', this.label.Store_is_Not_Authorized); this.showAuthorize = true; this.isStoreAuthorized = false; this.accessTokenAvailable = false; this.storeNameSelected = false; this.storeSelectionType = 'Edit'; } }).catch((error) => { this.closeSpinnerHandler(); }); } else { if (!this.storeUrl) { this.showNotification(this.label.Error, 'Error', this.label.Please_enter_store_name); } else if (!this.apiAccessToken) { this.showNotification(this.label.Error, 'Error', this.label.Please_enter_password); } } } //CHECK CREDENTIALS WHEN STORE IS EDIT OR NEW checkCredentials(shopifyUrl, PASSWORD) { console.log('SYNC MADE EASY: Credential => checkCredentials called, storeSelectionType: ' + this.storeSelectionType + ', isStoreAuthorized: ' + this.isStoreAuthorized + ', AccessScope: ' + this.AccessScope); if (this.storeSelectionType === 'Existing Store' && !this.AccessScope) { console.log('SYNC MADE EASY: Credential => Existing Store path => saveConfigrationFunction'); this.saveConfigrationFunction(); } else if (!this.isStoreAuthorized) { if (!shopifyUrl) { this.showNotification(this.label.Error, 'Error', this.label.Please_enter_store_name); } else if (!PASSWORD) { this.showNotification(this.label.Error, 'Error', this.label.Please_enter_password); } if (shopifyUrl && PASSWORD) { console.log('SYNC MADE EASY: Credential => Calling checkCreds Apex'); checkCreds({ 'shopifyUrl': shopifyUrl, 'PASSWORD': PASSWORD }).then((result) => { console.log('SYNC MADE EASY: Credential => checkCreds result: ' + result); if (result == true) { this.accessTokenAvailable = true; this.storeNameSelected = true; this.isStoreAuthorized = true; this.showAuthorize = false; this.remoteSiteForStore = false; this.checkAccessScope(shopifyUrl, PASSWORD); } else { console.log('SYNC MADE EASY: Credential => checkCreds returned false - spinner will be stuck here'); // open modal this.showNotification(this.label.Error, 'Error', this.label.Store_is_Not_Authorized); this.showAuthorize = true; this.isStoreAuthorized = false; this.accessTokenAvailable = false; this.storeNameSelected = false; this.storeSelectionType = 'Edit'; } }).catch((error) => { console.log('SYNC MADE EASY: Credential => checkCreds catch: ' + JSON.stringify(error)); this.closeSpinnerHandler(); }); } } else { console.log('SYNC MADE EASY: Credential => isStoreAuthorized=true path => checkAccessScope'); this.checkAccessScope(shopifyUrl, PASSWORD); } } //CHECK ACCESS SCOPE checkAccessScope(shopifyUrl, PASSWORD) { console.log('SYNC MADE EASY: Credential => Checking Access Scope'); checkAccesScope({ 'shopifyUrl': shopifyUrl, 'PASSWORD': PASSWORD }).then((result) => { this.listShopfyObjectAccessError = result; this.handleAccessScope(); }).catch((error) => { this.closeSpinnerHandler(); this.showNotification(this.label.Error, 'Error', this.label.Store_is_Not_having_Access); }); } handleAccessScope() { this.closeSpinnerHandler(); //this.showAuthorize = false; this.listAccessScopeError = []; this.AccessScope = false; if (this.listShopfyObjectAccessError) { if (this.productSyncValue && !this.listShopfyObjectAccessError.includes('read_products')) { this.AccessScope = true; if (!this.listAccessScopeError.includes('Product')) { this.listAccessScopeError.push('Product'); } } if (this.collectionSyncValue && !this.listShopfyObjectAccessError.includes('read_products')) { this.AccessScope = true; if (!this.listAccessScopeError.includes('collection')) { this.listAccessScopeError.push('collection'); } } if (this.inventorySyncValue && (!this.listShopfyObjectAccessError.includes('read_inventory') || !this.listShopfyObjectAccessError.includes('read_locations'))) { this.AccessScope = true; if (!this.listAccessScopeError.includes('Location/Inventory')) { this.listAccessScopeError.push('Location/Inventory'); } } if (this.customerSyncValue && !this.listShopfyObjectAccessError.includes('read_customers')) { this.AccessScope = true; if (!this.listAccessScopeError.includes('Customer')) { this.listAccessScopeError.push('Customer'); } } if (this.orderSyncValue && !this.listShopfyObjectAccessError.includes('read_orders')) { this.AccessScope = true; if (!this.listAccessScopeError.includes('Orders')) { this.listAccessScopeError.push('Orders'); } } if (this.draftSyncValue && !this.listShopfyObjectAccessError.includes('read_draft_orders')) { this.AccessScope = true; if (!this.listAccessScopeError.includes('Draft Orders')) { this.listAccessScopeError.push('Draft Orders'); } } } // if (!this.AccessScope) { // this.apiAccessToken = PASSWORD; // } else { // if (this.storeSelectionType !== 'Existing Store') { // this.showNotification(this.label.Error, 'Error', 'You have no Access in Shopify Org'); // } // } } //Updating store settings saveConfigrationFunction() { console.log('>>> saveConfigrationFunction'); console.log('SYNC MADE EASY: SAVE CONFIGURATIONS'); if (this.siteURL) { this.configObject.HIC_ShopifySync__Salesforce_Site_URL__c = this.siteURL; } if (this.storeUrl) { this.configObject.HIC_ShopifySync__Shopify_URL__c = this.storeUrl; } if ((this.storapiAccessTokeneUrl && this.storapiAccessTokeneUrl != '') || (this.apiAccessToken && this.apiAccessToken != '')) { this.configObject.HIC_ShopifySync__Private_Key_Password__c = this.apiAccessToken; } if (this.priceBook) { this.configObject.HIC_ShopifySync__Salesforce_Pricebook__c = this.priceBook; } this.configObject.HIC_ShopifySync__Product_Sync__c = this.productSyncValue; this.configObject.HIC_ShopifySync__Customer_Sync__c = this.customerSyncValue; this.configObject.HIC_ShopifySync__Collection_Sync__c = this.collectionSyncValue; this.configObject.HIC_ShopifySync__Collection__c = this.collectionSyncValue; this.configObject.HIC_ShopifySync__Inventory_Location__c = this.inventorySyncValue; this.configObject.HIC_ShopifySync__Inventory_Location_Sync__c = this.inventorySyncValue; this.configObject.HIC_ShopifySync__Order_Sync__c = this.orderSyncValue; // Order mapping handling (UNCHANGED) if (!this.orderSyncValue) { this.configObject.HIC_ShopifySync__Order__c = false; this.configObject.HIC_ShopifySync__Opportunity__c = false; this.configObject.HIC_ShopifySync__Order_Line_Item__c = false; } if ( this.orderSyncValue && ((!this.configObject.hasOwnProperty('HIC_ShopifySync__Order__c') || !this.configObject.HIC_ShopifySync__Order__c) && (!this.configObject.hasOwnProperty('HIC_ShopifySync__Opportunity__c') || !this.configObject.HIC_ShopifySync__Opportunity__c)) ) { this.configObject.HIC_ShopifySync__Order__c = true; this.configObject.HIC_ShopifySync__Opportunity__c = true; this.configObject.HIC_ShopifySync__Order_Line_Item__c = true; } // Opportunity Product if (this.configObject.HIC_ShopifySync__Opportunity__c === false) { this.configObject.HIC_ShopifySync__Opportunity_Line_Item__c = false; } else if ( this.configObject.HIC_ShopifySync__Order__c === false && this.configObject.HIC_ShopifySync__Opportunity__c ) { this.configObject.HIC_ShopifySync__Opportunity_Line_Item__c = true; } else if (this.configObject.HIC_ShopifySync__Order_Sync__c === true) { this.configObject.HIC_ShopifySync__Opportunity_Line_Item__c = true; } // Order fulfillment if (this.orderSyncChanged) { this.configObject.HIC_ShopifySync__Order_Fulfillment__c = this.orderSyncValue; } // Draft Order – SAVE ONLY (FIXED) this.configObject.HIC_ShopifySync__Draft_Order_Sync__c = this.draftSyncValue; // Safety only: Draft OFF → children OFF if (!this.draftSyncValue) { this.configObject.HIC_ShopifySync__Enable_Draft_Order__c = false; this.configObject.HIC_ShopifySync__Enable_Opportunity_for_SDO__c = false; this.configObject.HIC_ShopifySync__Specify_Close_Date__c = false; } // Draft Order Opportunity (New store rule – unchanged) if (this.storeSelectionType === 'New') { this.configObject.HIC_ShopifySync__Enable_Opportunity_for_SDO__c = true; this.configObject.HIC_ShopifySync__Specify_Close_Date__c = false; this.configObject.HIC_ShopifySync__Enable_Draft_Order__c = false; this.configObject.HIC_ShopifySync__Order__c = true; this.configObject.HIC_ShopifySync__Order_Line_Item__c = true; this.configObject.HIC_ShopifySync__Opportunity__c = false; this.configObject.HIC_ShopifySync__Opportunity_Line_Item__c = false; this.configObject.HIC_ShopifySync__Order_Fulfillment__c = false; } this.configObject.CurrencyIsoCode = this.storeSelectedCurrency; this.updatePriceBookCurrencyOpportunityStage(); } //Updating pricebook, Currency, Opportunity stage name updatePriceBookCurrencyOpportunityStage() { console.log('SYNC MADE EASY: SAVE ALL DETAILS'); let priceBookId = null; let currencyCode = null; let stageName = null; let configObject = JSON.stringify(this.configObject); if (this.isPricebookChanged || this.storeSelectionType === 'New') { priceBookId = this.priceBook; } if (this.isCurrencyChanged || this.storeSelectionType === 'New') { currencyCode = this.storeSelectedCurrency; } if (this.storeSelectionType === 'New') { stageName = 'New'; } if (this.storeSelectionType === 'Existing Store' && !this.collectionSyncChanged && !this.orderSyncChanged && !this.draftSyncChanged && !this.inventorySyncChanged && !this.isPricebookChanged && !this.isCurrencyChanged) { configObject = null; } if (this.AccessScope) { checkAccesScope({ 'shopifyUrl': this.storeUrl, 'PASSWORD': this.apiAccessToken }).then((result) => { this.listShopfyObjectAccessError = result; this.handleAccessScope(); if (!this.AccessScope) { if (priceBookId || currencyCode || stageName || configObject) { updatePriceBookCurrencyStage({ storeName: this.storeName, defaultStageName: stageName, priceBookId: priceBookId, currencyCode: currencyCode, configObj: configObject }).then((result) => { console.log('>>> ACCESS SCOPE REUSLT :: ' + JSON.stringify(result)); let proceedNext = true; if (stageName && !result.OppStageName) { proceedNext = false; } if (priceBookId && !result.PriceBook) { proceedNext = false; } if (currencyCode && !result.Currency) { proceedNext = false; } if (result.hasOwnProperty('ConfigObj') && !result.ConfigObj) { proceedNext = false; } if (proceedNext) { const selectStore = new CustomEvent('save', { detail: this.storeName }); this.dispatchEvent(selectStore); } else { this.closeSpinnerHandler(); } }).catch((error) => { this.closeSpinnerHandler(); }); } else { const selectStore = new CustomEvent('save', { detail: this.storeName }); this.dispatchEvent(selectStore); } } else { this.showNotification(this.label.Error, 'Error', this.label.You_do_not_have_access_of_selected_object); } }).catch((error) => { this.closeSpinnerHandler(); this.showNotification(this.label.Error, 'Error', this.label.Store_is_Not_having_Access); }); } else { if (priceBookId || currencyCode || stageName || configObject) { updatePriceBookCurrencyStage({ storeName: this.storeName, defaultStageName: stageName, priceBookId: priceBookId, currencyCode: currencyCode, configObj: configObject }).then((result) => { console.log('>>> THIS IS PRICEBOOK RESULT :: ' + JSON.stringify(result)); let proceedNext = true; if (stageName && !result.OppStageName) { proceedNext = false; } if (priceBookId && !result.PriceBook) { proceedNext = false; } if (currencyCode && !result.Currency) { proceedNext = false; } if (result.hasOwnProperty('ConfigObj') && !result.ConfigObj) { proceedNext = false; } if (proceedNext) { console.log('>>> ELSE PART STORE NAME :: ' + this.storeName); const selectStore = new CustomEvent('save', { detail: this.storeName }); this.dispatchEvent(selectStore); } else { this.closeSpinnerHandler(); } }).catch((error) => { this.closeSpinnerHandler(); }); } else { console.log('>>> ELSE PART STORE NAME :: ' + this.storeName); const selectStore = new CustomEvent('save', { detail: this.storeName }); this.dispatchEvent(selectStore); } } } /* * Helps to Show a Toast Message */ // Custom gradient toast (success = green→teal, error = red) instead of // ShowToastEvent. Same (title, variant, message) signature/callers. showNotification(title, variant, message) { this.closeSpinnerHandler(); const v = (variant || '').toString().toLowerCase(); const kind = v === 'error' ? 'error' : v === 'success' ? 'success' : v === 'warning' ? 'warning' : 'info'; const id = 't' + (this._toastSeq++); this.toasts = [...this.toasts, { id, title, message, cssClass: 'sw-toast sw-toast-' + kind, iconLabel: kind === 'error' ? '⚠' : kind === 'success' ? '✓' : kind === 'warning' ? '!' : 'ℹ' }]; if (kind !== 'error') { // eslint-disable-next-line @lwc/lwc/no-async-operation setTimeout(() => this._dismissToast(id), 4000); } } _dismissToast(id) { this.toasts = this.toasts.filter((t) => t.id !== id); } handleToastClose(event) { this._dismissToast(event.currentTarget.dataset.id); } //This Method is used to close spinner closeSpinnerHandler() { this.isLoading = false; const stopSpinner = new CustomEvent('closespinner', { detail: 'Stop Spinner' }); this.dispatchEvent(stopSpinner); } //This Method is used to show spinner showSpinnerHandler() { this.isLoading = true; const startSpinner = new CustomEvent('showspinner', { detail: 'Start Spinner' }); this.dispatchEvent(startSpinner); } //This method controls multistore modal visibility @api openMultiStoreController() { this.multiStoreModel = true; } //Edit Existing Store deleteStore() { console.log('SYNC MADE EASY: Credential => Deleting Store'); RemoveStore({ store: this.storeName }).then((result) => { if (result) { //Success } }).catch((error) => { //Error occured this.closeSpinnerHandler(); }); } createRemoteSiteForStore() { if (!this.storeName && !this.storeUrl) { this.showNotification(this.label.Error, 'Error', this.label.Please_enter_shopify_store_name); } else if (!this.apiAccessToken) { this.showNotification(this.label.Error, 'Error', this.label.Please_enter_admin_api_access_token); } else { this.showAuthorize = false; this.showSpinnerHandler(); checkStoreNameRemoteSite({ shopifyStoreUrl: this.storeUrl }).then((result) => { if (result) { this.remoteSiteForStore = false; if (this.storeSelectionType !== 'Existing Store' && !this.isStoreAuthorized) { this.checkCredentials(this.storeUrl, this.apiAccessToken); } else { this.validateCredentials(); } this.isRemoteSiteCreated = true; } else { this.closeSpinnerHandler(); this.isRemoteSiteCreated = false; this.remoteSiteForStore = true; this.showNotification(this.label.Error, 'Error', this.label.Create_Remote_site_Setting); var currentDateTime = new Date(); var datetime = currentDateTime.getFullYear() + ("0" + (currentDateTime.getMonth() + 1)).slice(-2) + ("0" + currentDateTime.getDate()).slice(-2) + ("0" + currentDateTime.getHours()).slice(-2) + ("0" + currentDateTime.getMinutes()).slice(-2) + ("0" + currentDateTime.getSeconds()).slice(-2); var siteName = 'HIC_SME_' + datetime; var serverUrlLink = 'https://' + this.storeName + '.myshopify.com'; window.open("/lightning/setup/SecurityRemoteProxy/page?address=/0rp/e?SiteName=" + siteName + "%26EndpointUrl=" + serverUrlLink, "_blank"); } console.log('@@@ checkStoreNameRemoteSite : ' + result); }).catch((error) => { this.closeSpinnerHandler(); console.log('@@@ Error checkStoreNameRemoteSite : ' + error); }); } } //Called on Check credentials button for not site supported orgs checkStoreNameRemoteSiteSetting() { this.showSpinnerHandler(); console.log('@@@ this.storeUrl checkStoreNameRemoteSite: ' + this.storeUrl); checkStoreNameRemoteSite({ shopifyStoreUrl: this.storeUrl }).then((result) => { if (result) { this.remoteSiteForStore = false; if (this.storeSelectionType !== 'Existing Store' && !this.isStoreAuthorized) { this.checkCredentials(this.storeUrl, this.apiAccessToken); } else { this.validateCredentials(); } this.isRemoteSiteCreated = true; } else { this.closeSpinnerHandler(); this.isRemoteSiteCreated = false; this.remoteSiteForStore = false; this.showAuthorize = true; this.showNotification(this.label.Error, 'Error', this.label.Create_Remote_site_Setting); } console.log('@@@ checkStoreNameRemoteSite : ' + result); }).catch((error) => { this.closeSpinnerHandler(); console.log('@@@ Error checkStoreNameRemoteSite : ' + error); }); } lockEmail(event) { const index = event.target.dataset.id; this.emailList = this.emailList.map(e => e.index == index && e.isValid ? { ...e, isEditable: false } : e ); } editEmail(event) { const index = event.target.dataset.id; this.emailList = this.emailList.map(e => e.index == index ? { ...e, isEditable: true } : e ); } get showEmailLimitText() { return this.emailList.length < 3; } updateDeleteState() { const canDelete = this.emailList.length > 1; this.emailList = this.emailList.map(email => { return { ...email, canDelete: canDelete }; }); } // Show credential action buttons only during first-time setup // (i.e. store name or access token not yet saved) get showCredentialActions() { return !(this.storeNameSelected && this.accessTokenAvailable); } // Generate Access Token button get disableGenerateAccessToken() { // Enabled only when store name exists AND access token is empty let checking = !this.storeName || !!this.apiAccessToken; this.disableInfo = !checking; return checking; } // Check Credential button get disableCheckCredential() { // Enabled only when BOTH exist return !(this.storeName && this.apiAccessToken); } fetchDefaultSiteUrl() { console.log('SYNC MADE EASY: Fetching Default Site URL from Configuration'); getDefaultSiteUrl() .then((result) => { if (result) { this.defaultSiteUrl = result; this.siteURL = result; console.log('>>> Default Site URL fetched: ' + result); } else { console.log('>>> No Default Site URL found in configuration'); this.siteURL = ''; } }) .catch((error) => { console.error('>>> Error fetching default site URL: ' + JSON.stringify(error)); this.siteURL = ''; }); } // ======================================================================== // NEW — presentation-only members added for the reference-mockup restyle. // Nothing above this line was changed from the original component. These // do not touch any Apex call or saved record. // ======================================================================== // Show/hide toggle for the Access Token field (mockup has an eye icon; // the original component only ever rendered type="password"). @track showToken = false; get tokenInputType() { return this.showToken ? 'text' : 'password'; } toggleTokenVisibility() { this.showToken = !this.showToken; } // Dynamic "You can add up to N more email(s)" hint — showEmailLimitText // (real getter above) only decides *whether* to show this text; the // label it used to point at was a static string that always said "3" // no matter how many were already in the list. This recomputes the // real remaining count off the same real emailList array. get emailLimitHintText() { this.stopEmailError = false; const remaining = 3 - this.emailList.length; if (remaining <= 0) { this.stopEmailError =true; return 'Maximum of 3 emails reached.'; } return `You can add up to ${remaining} more email${remaining === 1 ? '' : 's'}.`; } // Reference-matching "Add email address" behavior — mirrors the // reference mockup's onAddEmail exactly: the only gate is the total // count (max 3), full stop. No extra "confirm it first" requirement — // that was tried in an earlier pass and it backfired (a row left as // the literal placeholder text, or one the user never bothered to // open the pencil on, would permanently block adding a 3rd address). // Drops the new row in already in "view" mode with a placeholder // address, same as the mockup; the user clicks the pencil (editEmail) // to actually type their address. Keeps the same emailList/keyIndex/ // checkIndexNo bookkeeping the real save flow (validateEmails/ // saveEmailSettings) depends on — just changes how the row first // appears. handleAddEmailRow() { if (this.emailList.length >= 3) { return; } const indexNumber = ++this.keyIndex; this.addEmailAddress = false; const newItem = { index: indexNumber, value: 'new.address@example.com', addEmail: this.addEmailAddress, exists: false, isEditable: false, isValid: true }; this.emailList = [...this.emailList, newItem]; this.checkIndexNo = this.emailList.length - 1; if (this.emailList.length > 2) { this.addButtonVisible = false; } this.emailListChanged = true; this.updateDeleteState(); } // Commits the typed address AND locks the row back to view mode in one // step, computing validity fresh off the field's live value at blur // time. (The original two-handler approach — onchange updating a // separately-tracked isValid flag, onblur reading that flag — could // race: if blur fired before the change handler's state update landed, // the row would never lock and the input would stay open even after // clicking elsewhere on the page.) handleEmailBlur(event) { const index = event.target.dataset.id; const value = (event.target.value || '').trim(); const emailRegex = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/; const isValid = emailRegex.test(value); this.emailListChanged = true; this.emailList = this.emailList.map((e) => { if (e.index == index) { return { ...e, value, isValid, isEditable: !isValid }; } return e; }); } // Manual vs Auto Configuration — isAutomatic is the real @api property // already wired to changeConfigurationType()/the `automation` event. get isManual() { return !this.isAutomatic; } // Whether to show the Check Credential / Generate Access Token row. // checkCredential is the real, pre-existing flag from multiStoreHandlerEvent: // true when adding a New/Edit store (no valid saved credentials yet, so // checking/generating makes sense), false for an already-connected // Existing Store (there's nothing to check/generate — it's already // authorized). isManual keeps this scoped to Manual setup only, same as // before. This just restores that existing gating to the template — // no change to how/when checkCredential itself gets set. get showCheckCredentialRow() { return this.isManual && this.checkCredential; } // DISPLAY-ONLY: resolves the currently-selected pricebook Id (this.priceBook) // to its human-readable Name from priceBookList, so the Auto Configuration // summary reflects the real selection instead of a hardcoded "Standard". get selectedPricebookLabel() { const match = (this.priceBookList || []).find(p => p.value === this.priceBook); return match ? match.label : 'Standard'; } // ------------------------------------------------------------------ // Custom toggle switches (Auto Configuration header + the 4 live // Object Sync rows). lightning-input type="toggle" was tried first — // it briefly flashed blue on click before settling to a plain grey/ // default look, because SLDS's own toggle styling tokens don't // reliably follow the --slds-c-checkbox-* overrides across every org/ // browser. These plain button+span switches give full, guaranteed // control over the on/off colors (green/grey, matching the reference // mockup exactly) with no dependency on SLDS's internal styling hooks. // Each click calls the REAL, untouched change handler (changeDraftSync // etc.) with a hand-built { target: { checked } } object — the same // shape lightning-input's onchange would have passed — so none of the // underlying Apex-tied logic changes, only how the switch is drawn. // ------------------------------------------------------------------ get autoToggleTrackClass() { return this.isAutomatic ? 'obj-toggle-track obj-toggle-track_on' : 'obj-toggle-track'; } get draftToggleTrackClass() { return this.draftSyncValue ? 'obj-toggle-track obj-toggle-track_on' : 'obj-toggle-track'; } get orderToggleTrackClass() { return this.orderSyncValue ? 'obj-toggle-track obj-toggle-track_on' : 'obj-toggle-track'; } get collectionToggleTrackClass() { return this.collectionSyncValue ? 'obj-toggle-track obj-toggle-track_on' : 'obj-toggle-track'; } get inventoryToggleTrackClass() { return this.inventorySyncValue ? 'obj-toggle-track obj-toggle-track_on' : 'obj-toggle-track'; } handleToggleDraftSync() { this.changeDraftSync({ target: { checked: !this.draftSyncValue } }); } handleToggleOrderSync() { this.changeOrderSync({ target: { checked: !this.orderSyncValue } }); } handleToggleCollectionSync() { this.changeCollectionSync({ target: { checked: !this.collectionSyncValue } }); } handleToggleInventorySync() { this.changeInventorySync({ target: { checked: !this.inventorySyncValue } }); } // Connection status pill in the hero card — derived the same way the // reference mockup derives connLabel/connDot (connected == automatic), // but here it also reflects real authorization state once a store is // actually connected, so it's not purely decorative. get isConnected() { return this.isAutomatic || this.isStoreAuthorized || this.accessTokenAvailable; } get connectionPillClass() { return this.isConnected ? 'conn-pill conn-pill_on' : 'conn-pill conn-pill_off'; } get connectionLabel() { return this.isConnected ? 'Connected & syncing' : 'Not connected yet'; } // Header pill (top-right, next to the Auto Configuration toggle) — // this one reflects Manual vs Auto mode specifically, matching the // reference mockup's connLabel/connDot exactly ("Manual setup" in // amber when off, "Auto-configured" in green when on). Distinct from // connectionPillClass/connectionLabel above, which drive the hero // card's "Connected & syncing" pill and reflect actual store // authorization instead. get autoConfigPillClass() { return this.isAutomatic ? 'conn-pill conn-pill_on' : 'conn-pill conn-pill_off'; } get autoConfigPillLabel() { return this.isAutomatic ? 'Auto-configured' : 'Manual setup'; } // DISPLAY-ONLY: fetches the ready-formatted "Last order synced X ago" text // for the current store. The time math is done server-side in Apex using // Datetime.now(), so there's no JS timezone ambiguity. loadLastOrderSyncTime() { if (!this.storeName) return; getLastOrderSyncTime({ store: this.storeName }) .then((text) => { this._lastSyncedText = text || ''; }) .catch((error) => { console.log('SME: getLastOrderSyncTime error', JSON.stringify(error)); this._lastSyncedText = ''; }); } // Show the "Last order synced …" line ONLY when this store has actually // synced a paid order/opportunity into this org (Apex returned text). get showLastSynced() { return !!this._lastSyncedText; } get lastSyncedText() { return this._lastSyncedText || ''; } // ------------------------------------------------------------------ // "Check Credential" result modal — reference mockup shows a // "Checking credential…" state followed by a "Credential is valid" // confirmation. The reference's own version just fakes this with a // setTimeout; here it wraps the REAL checkStoreCredentials() flow // (verbatim, untouched — createStoreUrlRemoteSite -> validateCredentials // -> checkCreds Apex -> checkAccessScope), so the result shown is real. // Polling isLoading is a non-invasive way to know when that real chain // has finished without changing anything inside it: every path through // it (success or failure) ends by calling closeSpinnerHandler(), which // flips isLoading back to false. // ------------------------------------------------------------------ @track credModalOpen = false; @track credChecking = false; @track credResultSuccess = false; credCheckPollInterval; // The real checkStoreCredentials() chain flips the real `isLoading` // flag on and off via showSpinnerHandler()/closeSpinnerHandler() — // same as it does for Save & Next, initial store load, etc. Normally // that swaps this whole component's body to the big logo loading // view. But while the Check Credential modal is open, that swap was // ALSO happening underneath the modal at the same time (the modal is // a sibling at the template root, not nested inside the isLoading // check) — showing both the modal AND the blurred logo card behind // it. This getter is what the body-level loading view is actually // gated on now: suppress it specifically while credModalOpen owns the // loading state, but leave isLoading itself completely untouched so // every other flow (Save & Next, initial load) behaves exactly as // before. get showBodyLoading() { return this.isLoading && !this.credModalOpen; } handleCheckCredentialClick() { if (this.disableCheckCredential) { return; } this.credModalOpen = true; this.credChecking = true; this.credResultSuccess = false; // Kick off the real, untouched credential check. this.checkStoreCredentials(); this.clearCredCheckPoll(); const startedAt = Date.now(); this.credCheckPollInterval = setInterval(() => { const timedOut = Date.now() - startedAt > 20000; // safety net only if (!this.isLoading || timedOut) { this.clearCredCheckPoll(); this.credChecking = false; this.credResultSuccess = !!this.isStoreAuthorized; } }, 250); } clearCredCheckPoll() { if (this.credCheckPollInterval) { clearInterval(this.credCheckPollInterval); this.credCheckPollInterval = null; } } closeCredCheckModal() { this.credModalOpen = false; this.clearCredCheckPoll(); } // "{{ objCount }} of 6 on" badge on the Object Sync Settings card. get objOnCount() { return [ this.productSyncValue, this.customerSyncValue, this.draftSyncValue, this.orderSyncValue, this.collectionSyncValue, this.inventorySyncValue ].filter(Boolean).length; } // Check Credential / Generate Access Token button emphasis — the real // enablement rules already live in disableCheckCredential / // disableGenerateAccessToken; these just pick which one gets the // green "primary" treatment vs the muted outline treatment. get checkCredButtonClass() { return this.disableCheckCredential ? 'sw-btn sw-btn_muted' : 'sw-btn sw-btn_primary'; } get genTokenButtonClass() { return this.disableGenerateAccessToken ? 'sw-btn sw-btn_muted' : 'sw-btn sw-btn_primary'; } // Back button in the new footer — this component previously had no // in-page Back control (the parent's own bottom Back button is hidden // for this step via showButtons(false, true, false, true) in home.js). // Rather than duplicate home.js's real previousPage() logic, just // bubble a plain event and let home.js handle it with that same, // untouched method — wired via onback={previousPage} on the // tag in home.html. handleBackStep() { this.dispatchEvent(new CustomEvent('back')); } // Stop every timer the Generate Token flow may have started if this // component is ever torn down mid-flow (e.g. user jumps to another // step via the left nav while the OAuth popup is open). disconnectedCallback() { this.clearTokenGenTimers(); this.clearCredCheckPoll(); this.unlockBackgroundScroll(); if (this._syncTimerId) { clearInterval(this._syncTimerId); this._syncTimerId = null; } } @wire(getOrganizationName) wiredOrganizationName({ data, error }) { if (data) { this.organizationName = data; } else if (error) { console.error('Error fetching organization name:', error); } } }