SyncMadeEasy/force-app/main/default/lwc/shopifyCredentials/shopifyCredentials.js

2361 lines
102 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*****************************************************************************
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
// <c-shopify-credentials> 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);
}
}
}