File: //home/shrsglobal/www/wp-content/plugins/migrate-guru/js/migrateguru-onboarding.js
/**
* MigrateGuru Onboarding - WordPress Plugin Integration
* Handles the 3-step migration process with WordPress AJAX and Rails API integration
*/
(function($) {
'use strict';
// ==================== STATE MANAGEMENT ====================
const mgState = {
destinationUrl: '',
validatedSteps: {},
step2ValidatedKey: '',
step2KeyEdited: false
};
// ==================== UTILITY FUNCTIONS ====================
/**
* Show toast notification
*/
function showToast(message, type = 'success') {
const container = $('#mg-toast-container');
const iconSvg = type === 'success'
? '<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>'
: '<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>';
const toast = $('<div>').addClass('mg-toast ' + type).html(`
<svg class="mg-toast-icon ${type}" viewBox="0 0 20 20" fill="currentColor">
${iconSvg}
</svg>
<span class="mg-toast-message">${message}</span>
`);
container.append(toast);
// Auto-remove after 3 seconds
setTimeout(() => {
toast.addClass('removing');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
/**
* Open modal
*/
function openModal(modalId) {
$('#' + modalId).addClass('mg-active');
$('body').css('overflow', 'hidden');
}
/**
* Close modal
*/
function closeModal(modalId) {
$('#' + modalId).removeClass('mg-active');
$('body').css('overflow', '');
}
/**
* Copy text to clipboard
*/
async function copyToClipboard(text) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return true;
} else {
// Fallback method
const textArea = $('<textarea>')
.val(text)
.css({
position: 'fixed',
left: '-999999px'
})
.appendTo('body');
textArea[0].select();
const successful = document.execCommand('copy');
textArea.remove();
return successful;
}
} catch (err) {
return false;
}
}
/**
* Validate email format
* More comprehensive regex that checks for:
* - Valid local part (before @)
* - Valid domain part (after @)
* - At least one dot in domain
* - No spaces
* Note: HTML5 type="email" provides browser-level validation,
* and backend uses WordPress's is_email() function for final validation
*/
function isValidEmail(email) {
// More comprehensive email regex pattern
// Allows most valid email formats while rejecting obviously invalid ones
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
return emailRegex.test(email) && email.length <= 254; // RFC 5321 max length
}
/**
* Decode base64 string safely
*/
function decodeBase64(value) {
try {
return atob(value);
} catch (error) {
return null;
}
}
/**
* Escape text for safe HTML interpolation
*/
function escapeHtml(value = '') {
return $('<div>').text(value || '').html();
}
/**
* Format URL for compact display
*/
function formatUrlForDisplay(url = '') {
if (!url) {
return 'Unknown';
}
return url.replace(/^https?:\/\//i, '').replace(/\/$/, '');
}
/**
* Update Step 3 migration overview details
*/
function updateStep3Overview() {
const sourceUrl = mgOnboarding.currentSiteUrl || '';
const formattedSource = sourceUrl ? formatUrlForDisplay(sourceUrl) : '—';
const $sourceCard = $('#mg-step3-source-card');
const $sourceLink = $('#mg-step3-source-url');
if (sourceUrl) {
$sourceCard.attr('title', sourceUrl);
$sourceLink
.removeClass('mg-site-url-disabled')
.attr('href', sourceUrl)
.text(formattedSource);
} else {
$sourceCard.removeAttr('title');
$sourceLink
.addClass('mg-site-url-disabled')
.attr('href', '#')
.text('—');
}
const $destinationCard = $('#mg-step3-destination-card');
const $destinationTitle = $('#mg-step3-destination-title');
const $destinationLink = $('#mg-step3-destination-url');
// Get destination URL from state or data attribute (for persistence)
const $keyInput = $('#mg-key-input');
const destinationUrl = mgState.destinationUrl || $keyInput.data('destination-url') || '';
if (destinationUrl) {
// Ensure state is synced with data attribute
if (!mgState.destinationUrl) {
mgState.destinationUrl = destinationUrl;
}
const formattedDest = formatUrlForDisplay(destinationUrl);
$destinationCard.removeClass('mg-site-card-empty').attr('title', destinationUrl);
$destinationTitle.text('New Site');
$destinationLink
.removeClass('mg-site-url-disabled')
.attr('href', destinationUrl)
.text(formattedDest);
} else {
$destinationCard.addClass('mg-site-card-empty').removeAttr('title');
$destinationTitle.text('New Site');
$destinationLink
.addClass('mg-site-url-disabled')
.attr('href', '#')
.text('Validate the key in Step 2 to see the destination details.');
}
}
/**
* Validate destination migration key (mirrors bv_parse_api_key)
*/
function validateApiKey(key) {
const invalidResponse = (message) => ({
valid: false,
message: message || 'Invalid migration key. Please copy it again from your destination site.'
});
const decoded = decodeBase64(key);
if (!decoded) {
return invalidResponse('Migration key is not properly encoded.');
}
const parts = decoded.split(':');
if (parts.length < 2) {
return invalidResponse('Migration key appears to be incomplete.');
}
const version = parts.shift();
if (version !== 'v2') {
return invalidResponse('This migration key is from an older plugin version. Install the latest MigrateGuru plugin on both sites and copy the key again.');
}
const payload = parts.join(':');
let secret;
let url;
let plugname = '';
const decodeUrlOrThrow = (value) => {
const decodedUrl = decodeBase64(value);
if (!decodedUrl) {
throw new Error('Migration key URL is invalid.');
}
return decodedUrl;
};
try {
const inner = payload.split(':', 3);
if (inner.length < 2) {
return invalidResponse('Migration key appears to be incomplete.');
}
secret = inner[0];
url = decodeUrlOrThrow(inner[1]);
plugname = inner[2] || '';
} catch (error) {
return invalidResponse(error.message);
}
if (!secret || secret.length < 32) {
return invalidResponse('Migration key secret is invalid.');
}
if (!url) {
return invalidResponse('Migration key URL is invalid.');
}
return {
valid: true,
data: {
secret,
url,
plugname
}
};
}
/**
* Update step state
*/
function updateStepState(stepNumber, state) {
const stepCard = $('#mg-step' + stepNumber);
const badge = $('#mg-step' + stepNumber + '-badge');
const status = $('#mg-step' + stepNumber + '-status');
// Remove all state classes
stepCard.removeClass('mg-active mg-disabled mg-completed mg-collapsed');
// Add new state
switch (state) {
case 'active':
stepCard.addClass('mg-active');
break;
case 'completed':
stepCard.addClass('mg-completed mg-collapsed');
if (badge.length) {
badge.addClass('mg-completed').html('✓');
}
if (status.length) {
status.show();
}
break;
case 'expanded':
// Re-opened completed step - keep completed badge but expand content
stepCard.addClass('mg-completed');
stepCard.removeClass('mg-collapsed');
if (badge.length && badge.hasClass('mg-completed')) {
// Keep the checkmark
}
// Show status only if step is validated and (for Step 2) key hasn't been edited
if (status.length && mgState.validatedSteps[stepNumber]) {
if (stepNumber === 2) {
// For Step 2, only show status if key hasn't been edited
if (!mgState.step2KeyEdited) {
status.show();
} else {
status.hide();
}
} else {
// For other steps, show status if validated
status.show();
}
}
break;
case 'disabled':
stepCard.addClass('mg-disabled');
break;
}
}
/**
* Disable subsequent steps based on the active step
*/
function disableSubsequentSteps(activeStepNumber) {
// Disable steps based on which step is active
if (activeStepNumber === 1) {
// If step 1 is active, disable step 2 and step 3
updateStepState(2, 'disabled');
updateStepState(3, 'disabled');
} else if (activeStepNumber === 2) {
// If step 2 is active, disable step 3
updateStepState(3, 'disabled');
}
// If step 3 is active, no steps need to be disabled
}
/**
* Enable step
*/
function enableStep(stepNumber) {
updateStepState(stepNumber, 'active');
// Disable subsequent steps when enabling a step
disableSubsequentSteps(stepNumber);
// Smooth scroll to the step
// setTimeout(() => {
// $('html, body').animate({
// scrollTop: $('#mg-step' + stepNumber).offset().top - 100
// }, 500);
// }, 300);
}
/**
* Expand a collapsed step
*/
function expandStep(stepNumber) {
const stepCard = $('#mg-step' + stepNumber);
// Only expand if step is collapsed
if (!stepCard.hasClass('mg-collapsed')) {
return;
}
// Update state to expanded
updateStepState(stepNumber, 'expanded');
// Disable subsequent steps when expanding a step
disableSubsequentSteps(stepNumber);
// Step 2 specific: Pre-fill validated key if available and not edited
if (stepNumber === 2) {
const $keyInput = $('#mg-key-input');
const $step2Status = $('#mg-step2-status');
if (mgState.step2ValidatedKey && !mgState.step2KeyEdited) {
// Pre-fill the validated key
$keyInput.val(mgState.step2ValidatedKey);
// Show validation status
if ($step2Status.length && mgState.validatedSteps[2]) {
$step2Status.show();
}
// Restore destination URL if available
if (mgState.destinationUrl) {
updateStep3Overview();
}
} else if (mgState.step2KeyEdited) {
// Key was edited, hide validation status
if ($step2Status.length) {
$step2Status.hide();
}
}
}
// Smooth scroll to the step
// setTimeout(() => {
// $('html, body').animate({
// scrollTop: stepCard.offset().top - 100
// }, 500);
// }, 100);
}
/**
* Get site info data from hidden form
*/
function getSiteInfoData() {
const data = {};
$('#mg-site-info-form input[type="hidden"]').each(function() {
data[$(this).attr('name')] = $(this).val();
});
return data;
}
// ==================== MODAL: COPY KEY ====================
function setupCopyKeyModal() {
const $copyKeyBtn = $('#mg-copy-key-btn');
const $copyKeyModal = $('#mg-copy-key-modal');
const $modalKeyInput = $('#mg-modal-key-input');
const $toggleVisibilityBtn = $('#mg-toggle-key-visibility');
const $eyeIcon = $('#mg-eye-icon');
const $eyeOffIcon = $('#mg-eye-off-icon');
const $copyKeyFromModalBtn = $('#mg-copy-key-from-modal');
const $closeBtn = $copyKeyModal.find('.mg-modal-close');
const $overlay = $copyKeyModal.find('.mg-modal-overlay');
// Open modal
$copyKeyBtn.on('click', () => {
openModal('mg-copy-key-modal');
});
// Toggle key visibility
let isKeyVisible = false;
$toggleVisibilityBtn.on('click', () => {
isKeyVisible = !isKeyVisible;
$modalKeyInput.attr('type', isKeyVisible ? 'text' : 'password');
$eyeIcon.css('display', isKeyVisible ? 'none' : 'block');
$eyeOffIcon.css('display', isKeyVisible ? 'block' : 'none');
});
// Copy key from modal
$copyKeyFromModalBtn.on('click', async () => {
const success = await copyToClipboard(mgOnboarding.currentSiteKey);
if (success) {
showToast('Migration key copied to clipboard!', 'success');
closeModal('mg-copy-key-modal');
} else {
showToast('Failed to copy key. Please try again.', 'error');
}
});
// Close modal
$closeBtn.on('click', () => closeModal('mg-copy-key-modal'));
$overlay.on('click', () => closeModal('mg-copy-key-modal'));
}
// ==================== MODAL: GET KEY ====================
function setupGetKeyModal() {
const $getKeyBtn = $('#mg-get-key-btn');
const $getKeyModal = $('#mg-get-key-modal');
const $gotItBtn = $('#mg-got-it-btn');
const $closeBtn = $getKeyModal.find('.mg-modal-close');
const $overlay = $getKeyModal.find('.mg-modal-overlay');
const $videoIframe = $('#mg-get-key-video-iframe');
// Open modal
$getKeyBtn.on('click', () => {
openModal('mg-get-key-modal');
// Load/reload the video to start from beginning
const videoSrc = $videoIframe.data('src');
if (videoSrc) {
$videoIframe.attr('src', videoSrc);
}
});
// Close modal and stop video
const closeModalAndStopVideo = () => {
closeModal('mg-get-key-modal');
// Clear video src to stop playback
$videoIframe.attr('src', '');
};
$gotItBtn.on('click', closeModalAndStopVideo);
$closeBtn.on('click', closeModalAndStopVideo);
$overlay.on('click', closeModalAndStopVideo);
}
// ==================== STEP 1: INSTALL PLUGIN ====================
function setupStep1() {
const $step1ContinueBtn = $('#mg-step1-continue-btn');
$step1ContinueBtn.on('click', () => {
// Mark Step 1 as completed and validated
mgState.validatedSteps[1] = true;
updateStepState(1, 'completed');
// Enable Step 2
enableStep(2);
});
}
// ==================== STEP 2: VALIDATE KEY ====================
function setupStep2() {
const $keyInput = $('#mg-key-input');
const $validateKeyBtn = $('#mg-validate-key-btn');
const $keyError = $('#mg-key-error');
const $toggleStep2KeyBtn = $('#mg-toggle-step2-key-visibility');
const $eyeIconStep2 = $('.mg-eye-icon-step2');
const $eyeOffIconStep2 = $('.mg-eye-off-icon-step2');
// Toggle key visibility for Step 2
let isStep2KeyVisible = false;
$toggleStep2KeyBtn.on('click', () => {
isStep2KeyVisible = !isStep2KeyVisible;
$keyInput.attr('type', isStep2KeyVisible ? 'text' : 'password');
$eyeIconStep2.css('display', isStep2KeyVisible ? 'none' : 'block');
$eyeOffIconStep2.css('display', isStep2KeyVisible ? 'block' : 'none');
});
// Clear error on input and reset destination URL
$keyInput.on('input', () => {
const currentKey = $keyInput.val().trim();
$keyError.hide().text('');
$keyInput.removeClass('mg-error');
// Check if key has been edited from validated key
if (mgState.step2ValidatedKey) {
if (currentKey !== mgState.step2ValidatedKey) {
// Key differs from validated key
mgState.step2KeyEdited = true;
// Clear validation status
const $step2Status = $('#mg-step2-status');
if ($step2Status.length) {
$step2Status.hide();
}
// Clear validated step flag
mgState.validatedSteps[2] = false;
// Clear destination URL
mgState.destinationUrl = '';
$keyInput.removeData('destination-url');
updateStep3Overview();
} else if (currentKey === mgState.step2ValidatedKey && mgState.step2KeyEdited) {
// Key matches validated key again - restore validation status
mgState.step2KeyEdited = false;
mgState.validatedSteps[2] = true;
// Restore destination URL if available
const storedUrl = $keyInput.data('destination-url');
if (storedUrl) {
mgState.destinationUrl = storedUrl;
updateStep3Overview();
}
// Show validation status if step is expanded
const $step2Status = $('#mg-step2-status');
if ($step2Status.length && !$('#mg-step2').hasClass('mg-collapsed')) {
$step2Status.show();
}
}
} else {
// No validated key yet, just clear destination URL
mgState.destinationUrl = '';
$keyInput.removeData('destination-url');
updateStep3Overview();
}
});
// Validate key
$validateKeyBtn.on('click', () => {
const enteredKey = $keyInput.val().trim();
// Validate empty
if (!enteredKey) {
$keyError.text('Please enter a migration key').show();
$keyInput.addClass('mg-error');
return;
}
// Clear any errors
$keyError.hide();
$keyInput.removeClass('mg-error');
// Check if destination key matches current site key
if (enteredKey === mgOnboarding.currentSiteKey) {
$keyError.text('You cannot use the same site key. Please enter the migration key from your destination site (the site you are migrating to).').show();
$keyInput.addClass('mg-error');
return;
}
// Validate format (client side)
const validationResult = validateApiKey(enteredKey);
if (!validationResult.valid) {
$keyError.text(validationResult.message).show();
$keyInput.addClass('mg-error');
return;
}
// Disable button during validation
$validateKeyBtn.prop('disabled', true);
// Make AJAX call to validate keys
$.ajax({
url: mgOnboarding.ajaxUrl,
type: 'POST',
data: {
action: 'mg_validate_key',
nonce: mgOnboarding.nonce,
destination_key: enteredKey,
source_key: mgOnboarding.currentSiteKey
},
success: function(response) {
if (response.success) {
// Store validated key and mark as validated
mgState.step2ValidatedKey = enteredKey;
mgState.step2KeyEdited = false;
mgState.validatedSteps[2] = true;
// Store destination URL in state and as data attribute for persistence
mgState.destinationUrl = validationResult.data.url;
$keyInput.data('destination-url', validationResult.data.url);
updateStep3Overview();
// Mark Step 2 as completed
updateStepState(2, 'completed');
// Enable Step 3
enableStep(3);
} else {
$keyError.text(response.data.message || 'Validation failed. Please try again.').show();
$keyInput.addClass('mg-error');
}
},
error: function(xhr, status, error) {
$keyError.text('An error occurred during validation. Please try again.').show();
$keyInput.addClass('mg-error');
},
complete: function() {
$validateKeyBtn.prop('disabled', false);
}
});
});
// Allow Enter key to validate
$keyInput.on('keypress', (e) => {
if (e.which === 13) {
$validateKeyBtn.click();
}
});
}
// ==================== STEP 3: INITIATE MIGRATION ====================
function setupStep3() {
const $emailInput = $('#mg-email-input');
const $initiateMigrationBtn = $('#mg-initiate-migration-btn');
const $emailError = $('#mg-email-error');
// Clear error on input - preserve destination URL state
$emailInput.on('input', () => {
$emailError.hide().text('');
$emailInput.removeClass('mg-error');
// Ensure destination URL is preserved from data attribute if state is lost
const $keyInput = $('#mg-key-input');
const storedDestinationUrl = $keyInput.data('destination-url');
if (storedDestinationUrl && !mgState.destinationUrl) {
mgState.destinationUrl = storedDestinationUrl;
}
// Update overview to ensure destination details are displayed
updateStep3Overview();
});
// Initiate migration
$initiateMigrationBtn.on('click', () => {
const email = $emailInput.val().trim();
const destinationKey = $('#mg-key-input').val().trim();
// Validate empty
if (!email) {
$emailError.text('Please enter your email address').show();
$emailInput.addClass('mg-error');
return;
}
// Validate format
if (!isValidEmail(email)) {
$emailError.text('Please enter a valid email address').show();
$emailInput.addClass('mg-error');
return;
}
// Ensure destination key is still valid
const destinationKeyValidation = validateApiKey(destinationKey);
if (!destinationKeyValidation.valid) {
showToast(destinationKeyValidation.message, 'error');
return;
}
// Clear any errors
$emailError.hide();
$emailInput.removeClass('mg-error');
// Disable button and show loading state
$initiateMigrationBtn.prop('disabled', true).html(`
<svg class="mg-spinner mg-btn-icon" width="16" height="16" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.5" fill="none" stroke-dasharray="40" stroke-dashoffset="0"/>
</svg>
Starting Migration...
`);
// Get site info data
const siteInfoData = getSiteInfoData();
// Make AJAX call to initiate migration
$.ajax({
url: mgOnboarding.ajaxUrl,
type: 'POST',
data: {
action: 'mg_initiate_migration',
nonce: mgOnboarding.nonce,
email: email,
destination_key: destinationKey,
source_key: mgOnboarding.currentSiteKey,
site_info: siteInfoData
},
success: function(response) {
if (response.success) {
showToast(response.data.message || 'Migration initiated successfully! Check your email for updates.', 'success');
// Mark Step 3 as completed and validated
mgState.validatedSteps[3] = true;
updateStepState(3, 'completed');
// Update button state
$initiateMigrationBtn.html('Migration In Progress...');
// Redirect to URL if provided in response
if (response.data.url) {
setTimeout(() => {
window.location.href = response.data.url;
}, 1500); // Small delay to show the success message
}
} else {
// Show error
showToast(response.data.message || 'Failed to initiate migration. Please try again.', 'error');
// Restore button
$initiateMigrationBtn.prop('disabled', false).html(`
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" class="mg-btn-icon">
<path d="M8 2v12M2 8h12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Initiate Migration
`);
}
},
error: function(xhr, status, error) {
showToast('An error occurred. Please try again.', 'error');
// Restore button
$initiateMigrationBtn.prop('disabled', false).html(`
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" class="mg-btn-icon">
<path d="M8 2v12M2 8h12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Initiate Migration
`);
}
});
});
// Allow Enter key to submit
$emailInput.on('keypress', (e) => {
if (e.which === 13) {
$initiateMigrationBtn.click();
}
});
}
// ==================== STEP HEADER CLICK HANDLERS ====================
function setupStepHeaderClicks() {
// Add click handlers to all step headers for collapsed steps
$('.mg-step-header').on('click', function(e) {
// Don't trigger if clicking on buttons or links inside the header
if ($(e.target).closest('button, a').length > 0) {
return;
}
const $header = $(this);
const $stepCard = $header.closest('.mg-step-card');
const stepId = $stepCard.attr('id');
// Extract step number from ID (e.g., 'mg-step2' -> 2)
const stepNumber = parseInt(stepId.replace('mg-step', ''), 10);
// Only expand if step is collapsed
if ($stepCard.hasClass('mg-collapsed')) {
expandStep(stepNumber);
}
});
}
// ==================== INITIALIZE ====================
$(document).ready(function() {
// Setup modals
setupCopyKeyModal();
setupGetKeyModal();
// Setup steps
setupStep1();
setupStep2();
setupStep3();
// Setup step header click handlers
setupStepHeaderClicks();
// Initialize Step 3 overview with source URL
updateStep3Overview();
// Initialize step disabling based on active step
// Check which step is currently active/expanded and disable subsequent steps
const $step1 = $('#mg-step1');
const $step2 = $('#mg-step2');
const $step3 = $('#mg-step3');
if ($step1.hasClass('mg-active') && !$step1.hasClass('mg-collapsed')) {
// Step 1 is active and not collapsed, disable step 2 and 3
disableSubsequentSteps(1);
} else if ($step2.hasClass('mg-active') && !$step2.hasClass('mg-collapsed')) {
// Step 2 is active and not collapsed, disable step 3
disableSubsequentSteps(2);
} else if ($step2.hasClass('mg-completed') && !$step2.hasClass('mg-collapsed')) {
// Step 2 is completed and expanded, disable step 3
disableSubsequentSteps(2);
} else if ($step1.hasClass('mg-completed') && !$step1.hasClass('mg-collapsed')) {
// Step 1 is completed and expanded, disable step 2 and 3
disableSubsequentSteps(1);
}
// Close modals on Escape key
$(document).on('keydown', (e) => {
if (e.key === 'Escape') {
closeModal('mg-copy-key-modal');
closeModal('mg-get-key-modal');
// Stop video when closing modal with Escape
$('#mg-get-key-video-iframe').attr('src', '');
}
});
});
})(jQuery);