diff --git a/assets/css/admin/facebook-for-woocommerce-whatsapp-banner.css b/assets/css/admin/facebook-for-woocommerce-whatsapp-banner.css new file mode 100644 index 000000000..29aac8249 --- /dev/null +++ b/assets/css/admin/facebook-for-woocommerce-whatsapp-banner.css @@ -0,0 +1,64 @@ +.fb-wa-banner { + display: block; + position: relative; + box-sizing: border-box; + width: 1000px; + margin-bottom: 20px; + padding: 16px; + color: #050505; + background: #fff; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); +} + +.fb-wa-banner p, +.fb-wa-banner h2 { + font-family: Helvetica, Arial, sans-serif; + font-size: 20px; + line-height: 24px; + color:#606770; +} + +.fb-wa-banner h2 { + font-weight: 600; + color: #1c1e21; +} + +.fb-wa-banner .wa-cta-button { + display: inline-block; + cursor: pointer; + border: 1px solid; + width: 151px; + border-radius: 6px; + text-decoration: none; + box-sizing: content-box; + font-size: 16px; + line-height: 34px; + -webkit-font-smoothing: antialiased; + font-weight: bold; + justify-content: center; + padding: 0 8px; + position: relative; + text-align: center; + text-shadow: none; + vertical-align: middle; + transition: 200ms cubic-bezier(.08, .52, .52, 1) background-color, 200ms cubic-bezier(.08, .52, .52, 1) box-shadow, 200ms cubic-bezier(.08, .52, .52, 1) transform; + padding: 0 16px; + color: #fff; + background-color: #1877f2; + border-color: #1877f2; +} + +.fb-wa-banner .wa-cta-button:hover { + background-color: #1763cf; + border-color: #1763cf; +} + +.fb-wa-banner .wa-close-button { + position: absolute; + display: block; + width: 16px; + height: 16px; + padding: 5px; + right: 16px; + top: 16px; +} diff --git a/assets/css/admin/facebook-for-woocommerce-whatsapp-utility.css b/assets/css/admin/facebook-for-woocommerce-whatsapp-utility.css new file mode 100644 index 000000000..5d3377513 --- /dev/null +++ b/assets/css/admin/facebook-for-woocommerce-whatsapp-utility.css @@ -0,0 +1,253 @@ +.onboarding-card { + background-color: #f7f7f7; + border: 1px solid #ccc; + border-radius: 10px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + width: 680px; + margin: 40px auto 0; /* Top margin 5px, horizontal centering */ +} +.custom-dashicon-check { + position: relative; + display: inline-block; + width: 26px; /* Set the size of the circle */ + height: 26px; /* Set the size of the circle */ + background-color: #1a805b; /* Fill the circle with green */ + border-radius: 50%; /* Make it a circle */ + margin-right: 20px; + top: 50%; + transform: translateY(-50%); +} +.custom-dashicon-check::before { + content: '\f147'; /* Unicode for dashicons-yes-alt */ + font-family: 'Dashicons'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-55%, -45%) scale(1.2); /* Center and slightly enlarge the checkmark */ + font-size: 20px; /* Set the size of the checkmark */ + color: white; /* Make the checkmark white */ + text-shadow: + -4px 0 #1a805b, + 4px 0 #1a805b, + 0 -2px #1a805b, + 0 2px #1a805b; /* Increase shadow offsets to thin the checkmark more */ +} + +.custom-dashicon-circle { + position: relative; + display: inline-block; + width: 20px; /* Set the size of the circle */ + height: 20px; /* Set the size of the circle */ + border-radius: 50%; /* Make it a circle */ + margin-right: 20px; + border: 3px solid #222121ab; + top: 50%; + transform: translateY(-50%); +} +.custom-dashicon-halfcircle { + position: relative; + display: inline-block; + width: 20px; /* Set the size of the circle */ + height: 20px; /* Set the size of the circle */ + border-radius: 50%; /* Make it a circle */ + margin-right: 20px; + border: 3px solid #222121ab; + background-image: linear-gradient(to left, #222121ab 50%, transparent 50%); + background-clip: padding-box; /* Add this line */ + top: 50%; + transform: translateY(-50%); +} +.card-content-icon { + display: flex; +} +.card-item { + padding: 10px 24px; + justify-content: space-between; + display: flex; +} +.divider { + border-bottom: 1px solid #ccc; +} +.review-payment-content { + padding: 20px; + margin-bottom: 10px; +} +.whatsapp-onboarding-button { + margin-left: auto; + position: relative; + top: 50%; + margin: auto 0; /* Ensure button is centered */ +} +.whatsapp-onboarding-done-button { + margin-left: auto; + padding: 6px 0; +} +.card-content { + max-width: 90%; +} +.card-content-icon h2 { + top: 50%; +} +.card-content-icon p { + margin-top: -10px; /* Remove margin top */ +} +.event-config { + display: flex; + flex-direction: row; + padding-top: 16px; + padding-bottom: 16px; +} +.event-config-heading-container { + display: flex; + flex-direction: row; +} +.event-config-manage-button { + margin-left: auto; + padding-left: 20px; + padding-right: 10px; + display: flex; + align-items: center; +} +.event-config-status { + background-color: #FFFFFF; + border: 1px solid #9f9f9f; + color: #9f9f9f; + padding: 4px 10px; + text-align: center; + display: inline-block; + border-radius: 16px; + margin-left: 10px; + font-size: small; + align-self: center; +} +.on-status { + background-color: #00A32A; + color: #FFFFFF; + border:none; +} +.manage-event-card-item { + padding: 20px; + justify-content: space-between; +} +.manage-event-selector { + min-width: 100%; +} +.manage-event-template-block { + border: 1px solid #c4c3c3; + margin-bottom: 20px; +} +.manage-event-template-header { + position: relative; + display: block; + padding: 20px; + font-size: medium; +} +.manage-event-template-footer { + padding: 20px; + display: flex; + flex-direction: row-reverse; + justify-content: flex-start; +} +.manage-event-button { + margin-left: 20px; +} +.manage-event-error-notice { + margin-right: 5px; +} +.fbwa-hidden-element { + display: none; +} +.error-notice-wrapper { + justify-content: left; + padding-left: 20px; + padding-bottom: 10px; + margin-right: 20px; /* Add the right margin */ +} +.notice-error { + background-color: #f7f7f7; + border: 1px solid #EF0000; /* Red border */ + border-radius: 0; /* No curvature */ + border-left-width: 5px; /* Thicker left border */ + width: 100%; /* Take up full width */ +} +.notice-error p { + margin: 5px; +} +.warning-custom-modal { + display: none; /* Hidden by default */ + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.4); /* Black with opacity */ +} +/* Modal content */ +.warning-modal-content { + background-color: #fefefe; + margin: 25% auto; + padding: 20px; + border: 1px solid #ddd; + top: 10%; + width: 50%; + max-width: 500px; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0,0,0,0.1); +} +/* Modal body */ +.warning-modal-body { + padding: 20px 0; +} +/* Modal footer */ +.warning-modal-footer { + padding: 10px 0; + text-align: right; +} +/* Close button */ +.warning-modal-close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; + position: absolute; + right: 0; + top: 0; +} +.warning-modal-close:hover { + color: #000; +} +.whatsapp-icon { + width: 45px; + height: 40px; + flex-shrink: 0; /* Prevents icon from shrinking */ +} +.contact-info { + padding-left: 10px; + display: flex; + flex-direction: column; +} +.contact-info h3 { + margin: 0; + font-size: 1.1em; +} +.contact-info p { + margin: 0; + font-size: 1.1em; + color: #666; +} +.disconnect-footer-left { + display: flex; + padding: 25px; +} +.disconnect-footer-right-separator { + margin-right:10px; +} +.disconnect-footer-right { + padding: 30px; + margin-left: auto; +} +.disconnect-footer { + display: flex; + position: relative; +} diff --git a/assets/images/ico-close.svg b/assets/images/ico-close.svg new file mode 100644 index 000000000..4929537ca --- /dev/null +++ b/assets/images/ico-close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/ico-whatsapp.png b/assets/images/ico-whatsapp.png new file mode 100644 index 000000000..9bf475436 Binary files /dev/null and b/assets/images/ico-whatsapp.png differ diff --git a/assets/images/whatsapp_icon.png b/assets/images/whatsapp_icon.png new file mode 100644 index 000000000..7637f5b15 Binary files /dev/null and b/assets/images/whatsapp_icon.png differ diff --git a/assets/js/admin/whatsapp-admin-banner.js b/assets/js/admin/whatsapp-admin-banner.js new file mode 100644 index 000000000..f11bb94a3 --- /dev/null +++ b/assets/js/admin/whatsapp-admin-banner.js @@ -0,0 +1,13 @@ +jQuery(function ($) { + $(document).on('click', '.fb-wa-banner .wa-close-button', function (e) { + e.preventDefault(); + + $.post(WCFBAdminBanner.ajax_url, { + action: 'wc_facebook_dismiss_banner', + nonce: WCFBAdminBanner.nonce, + banner_id: WCFBAdminBanner.banner_id + }).done(function (response) { + $('.fb-wa-banner').remove(); + }); + }); +}); diff --git a/assets/js/admin/whatsapp-admin-notice.js b/assets/js/admin/whatsapp-admin-notice.js new file mode 100644 index 000000000..2cb5057be --- /dev/null +++ b/assets/js/admin/whatsapp-admin-notice.js @@ -0,0 +1,9 @@ +jQuery(function ($) { + $(document).on('click', '.wc-facebook-global-notice.is-dismissible .notice-dismiss', function () { + $.post(WCFBAdminNotice.ajax_url, { + action: 'wc_facebook_dismiss_notice', + nonce: WCFBAdminNotice.nonce, + notice_id: WCFBAdminNotice.notice_id + }); + }); +}); diff --git a/assets/js/admin/whatsapp-billing.js b/assets/js/admin/whatsapp-billing.js new file mode 100644 index 000000000..3ba8c2bb6 --- /dev/null +++ b/assets/js/admin/whatsapp-billing.js @@ -0,0 +1,50 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + * @package FacebookCommerce + */ + +jQuery( document ).ready( function( $ ) { + var $billingStepInProgress = $('#wc-fb-whatsapp-billing-inprogress'); + var $billingStepNotStarted = $('#wc-fb-whatsapp-billing-notstarted'); + var $billingStepSuccess = $('#wc-fb-whatsapp-billing-success'); + var $billingSubcontent = $('#wc-fb-whatsapp-billing-subcontent'); + var $billingButtonWrapper = $('#wc-fb-whatsapp-billing-button-wrapper'); + var $whatsappOnboardingDoneButton = $('#whatsapp-onboarding-done-button'); + if (facebook_for_woocommerce_whatsapp_billing.consent_collection_enabled) { + facebook_for_woocommerce_whatsapp_billing.is_payment_setup ? $billingStepSuccess.show() : $billingStepInProgress.show(); + $whatsappOnboardingDoneButton.show(); + $billingStepNotStarted.hide(); + } else { + $billingStepInProgress.hide(); + $billingStepNotStarted.show(); + $billingSubcontent.hide(); + $whatsappOnboardingDoneButton.hide(); + $billingButtonWrapper.hide() + } + + // handle the whatsapp add payment button click should open billing flow in Meta + $('#wc-whatsapp-add-payment').click(function(event) { + + $.post( facebook_for_woocommerce_whatsapp_billing.ajax_url, { + action: 'wc_facebook_whatsapp_fetch_url_info', + nonce: facebook_for_woocommerce_whatsapp_billing.nonce + }, function ( response ) { + if ( response.success ) { + console.log( 'Whatsapp Billing Url Info Fetched Successfully', response ); + var business_id = response.data.business_id; + var asset_id = response.data.waba_id; + const BILLING_URL = `https://business.facebook.com/billing_hub/accounts/details/?business_id=${business_id}&asset_id=${asset_id}&account_type=whatsapp-business-account`; + window.open( BILLING_URL); + } else { + console.log( 'Whatsapp Billing Url Info Fetch Failure', response ); + } + } ); + + + }); + +} ); diff --git a/assets/js/admin/whatsapp-connection.js b/assets/js/admin/whatsapp-connection.js new file mode 100644 index 000000000..e1bf1ad26 --- /dev/null +++ b/assets/js/admin/whatsapp-connection.js @@ -0,0 +1,67 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + * @package FacebookCommerce + */ + +jQuery( document ).ready( function( $ ) { + var $connectSuccess = $('#wc-fb-whatsapp-connect-success'); + var $connectInProgress = $('#wc-fb-whatsapp-connect-inprogress'); + var $connectSubcontent = $('#wc-fb-whatsapp-onboarding-subcontent'); + var $connectButtonWrapper = $('#wc-fb-whatsapp-onboarding-button-wrapper'); + if (facebook_for_woocommerce_whatsapp_onboarding_progress.whatsapp_onboarding_complete) { + $connectSuccess.show(); + $connectInProgress.hide(); + $connectSubcontent.hide(); + $connectButtonWrapper.hide(); + } else { + $connectSuccess.hide(); + $connectInProgress.show(); + } + + // handle the whatsapp connect button click should open hosted ES flow + $( '#woocommerce-whatsapp-connection' ).click( function( event ) { + const APP_ID = '474166926521348'; // WOO_COMMERCE_APP_ID + const CONFIG_ID = '1237758981048330'; // WOO_COMMERCE_WHATSAPP_CONFIG_ID + const HOSTED_ES_URL = `https://business.facebook.com/messaging/whatsapp/onboard/?app_id=${APP_ID}&config_id=${CONFIG_ID}`; + window.open( HOSTED_ES_URL); + updateProgress(0,1800000); // retry for 30 minutes + }); + + function updateProgress(retryCount = 0, pollingTimeout = 1800000) { + $.post( facebook_for_woocommerce_whatsapp_onboarding_progress.ajax_url, { + action: 'wc_facebook_whatsapp_onboarding_progress_check', + nonce: facebook_for_woocommerce_whatsapp_onboarding_progress.nonce + }, function ( response ) { + + // check if the response is success (i.e. onboarding is completed) + if ( response.success ) { + console.log( 'Whatsapp Connection is Complete', response ); + // update the progress for connect whatsapp step + $connectInProgress.remove(); + $connectSuccess.show(); + // collapse whatsapp onboarding step subcontect and button on success + $connectSubcontent.hide(); + $connectButtonWrapper.hide(); + // update the progress for collect consent step and show button and subcontent + $('#wc-fb-whatsapp-consent-collection-inprogress').show(); + $('#wc-fb-whatsapp-consent-collection-notstarted').hide(); + $('#wc-fb-whatsapp-consent-subcontent').show(); + $('#wc-fb-whatsapp-consent-button-wrapper').show(); + + } else { + console.log('Whatsapp connection is not complete. Checking again in 5 seconds:', response, ', retry attempt:', retryCount, 'pollingTimeout', pollingTimeout); + if(retryCount >= pollingTimeout) { + console.log('Max retries reached. Aborting.'); + return; + } + setTimeout( function() { updateProgress(retryCount + 1, pollingTimeout); }, 5000 ); + } + } ); + + } + +} ); diff --git a/assets/js/admin/whatsapp-consent-remove.js b/assets/js/admin/whatsapp-consent-remove.js new file mode 100644 index 000000000..446af1299 --- /dev/null +++ b/assets/js/admin/whatsapp-consent-remove.js @@ -0,0 +1,105 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + * @package FacebookCommerce + */ + +jQuery( document ).ready( function( $ ) { + // Get the modal and related elements + var modal = document.getElementById("wc-fb-warning-modal"); + var cancelButton = document.getElementById("wc-fb-warning-modal-cancel"); + var confirmButton = document.getElementById("wc-fb-warning-modal-confirm"); + var $statusElement = $('#wc-whatsapp-collect-consent-status'); + + if (!facebook_for_woocommerce_whatsapp_consent.consent_collection_enabled) { + // Change the status from "on-status" to "off-status" for the specific element. + $statusElement.removeClass('on-status').addClass('off-status'); + // Update the text to "Off". + $statusElement.text('Off'); + // Hide the original "Remove" button + $('#wc-whatsapp-collect-consent-remove-container').addClass('fbwa-hidden-element'); + // Show the "Add" button + $('#wc-whatsapp-collect-consent-add-container').removeClass('fbwa-hidden-element'); + } + + // On click of the remove button, show the warning modal + $("#wc-whatsapp-collect-consent-remove").click(function(event) { + // Show the modal + modal.style.display = "block"; + + // Prevent default action + event.preventDefault(); + }); + + if (cancelButton) { + // Close modal when clicking the Cancel button + cancelButton.onclick = function() { + modal.style.display = "none"; + }; + } + + if (confirmButton) { + // Handle confirm action + confirmButton.onclick = function() { + // Send the AJAX request to disable WhatsApp consent collection + $.post(facebook_for_woocommerce_whatsapp_consent_remove.ajax_url, { + action: 'wc_facebook_whatsapp_consent_collection_disable', + nonce: facebook_for_woocommerce_whatsapp_consent_remove.nonce + }, function(response) { + if (response.success) { + console.log( 'Whatsapp Consent Collection Disabled Successfully', response ); + // Change the status from "on-status" to "off-status" for the specific element. + $statusElement.removeClass('on-status').addClass('off-status'); + // Update the text to "Off". + $statusElement.text('Off'); + + // Hide the original "Remove" button + $('#wc-whatsapp-collect-consent-remove-container').addClass('fbwa-hidden-element'); + + // Show the "Add" button + $('#wc-whatsapp-collect-consent-add-container').removeClass('fbwa-hidden-element'); + } else { + console.log( 'Whatsapp Consent Collection Disabling Failed', response ); + } + }); + + // Close the modal + modal.style.display = "none"; + }; + } + + // Add event listener to the "Add" button + $('#wc-whatsapp-collect-consent-add').click(function() { + // Send the AJAX request to enable WhatsApp consent collection + $.post(facebook_for_woocommerce_whatsapp_consent.ajax_url, { + action: 'wc_facebook_whatsapp_consent_collection_enable', + nonce: facebook_for_woocommerce_whatsapp_consent.nonce + }, function(response) { + if (response.success) { + console.log( 'Whatsapp Consent Collection Enabled Successfully', response ); + // Change the status from "off-status" to "on-status" for the specific element. + $statusElement.removeClass('off-status').addClass('on-status'); + // Update the text to "On". + $statusElement.text('On'); + + // Hide the "Add" button + $('#wc-whatsapp-collect-consent-add-container').addClass('fbwa-hidden-element'); + + // Show the original "Remove" button + $('#wc-whatsapp-collect-consent-remove-container').removeClass('fbwa-hidden-element'); + } else { + console.log( 'Whatsapp Consent Collection Enabling Failed', response ); + } + }); + }); + + // Close modal when clicking outside of it + window.onclick = function(event) { + if (event.target == modal) { + modal.style.display = "none"; + } + }; +}); diff --git a/assets/js/admin/whatsapp-consent.js b/assets/js/admin/whatsapp-consent.js new file mode 100644 index 000000000..ee64c60c2 --- /dev/null +++ b/assets/js/admin/whatsapp-consent.js @@ -0,0 +1,83 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + * @package FacebookCommerce + */ + +jQuery( document ).ready( function( $ ) { + var $consentCollectSuccess = $('#wc-fb-whatsapp-consent-collection-success'); + var $consentCollectInProgress = $('#wc-fb-whatsapp-consent-collection-inprogress'); + var $consentCollectNotStarted = $('#wc-fb-whatsapp-consent-collection-notstarted'); + var $consentSubcontent = $('#wc-fb-whatsapp-consent-subcontent'); + var $consentButtonWrapper = $('#wc-fb-whatsapp-consent-button-wrapper'); + if (facebook_for_woocommerce_whatsapp_consent.whatsapp_onboarding_complete) { + if (facebook_for_woocommerce_whatsapp_consent.consent_collection_enabled) { + showConsentCollectionProgressIcon(true, false, false); + $consentSubcontent.hide(); + $consentButtonWrapper.hide(); + } else { + showConsentCollectionProgressIcon(false, true, false); + } + } else { + showConsentCollectionProgressIcon(false, false, true); + $consentSubcontent.hide(); + $consentButtonWrapper.hide(); + } + + // handle the whatsapp consent collect button click should save setting to wp_options table + $( '#wc-whatsapp-collect-consent' ).click( function( event ) { + + $.post( facebook_for_woocommerce_whatsapp_consent.ajax_url, { + action: 'wc_facebook_whatsapp_consent_collection_enable', + nonce: facebook_for_woocommerce_whatsapp_consent.nonce + }, function ( response ) { + if ( response.success ) { + console.log( 'Whatsapp Consent Collection is Enabled in Checkout Flow', response ); + // update the progress for collect consent step and hide the button and subcontent + showConsentCollectionProgressIcon(true, false, false); + $consentSubcontent.hide(); + $consentButtonWrapper.hide(); + // update the progress of billing step and show the button and subcontent + if(response.data['is_payment_setup'] === true) { + $('#wc-fb-whatsapp-billing-inprogress').hide(); + $('#wc-fb-whatsapp-billing-notstarted').hide(); + $('#wc-fb-whatsapp-billing-success').show(); + } else { + $('#wc-fb-whatsapp-billing-inprogress').show(); + $('#wc-fb-whatsapp-billing-notstarted').hide(); + + } + $('#wc-fb-whatsapp-billing-subcontent').show(); + $('#wc-fb-whatsapp-billing-button-wrapper').show(); + $('#whatsapp-onboarding-done-button').show(); + } else { + console.log( 'Whatsapp Consent Collection Enabling has Failed', response ); + } + } ); + + }); + + function showConsentCollectionProgressIcon(success, inProgress, notStarted) { + if (success) { + $consentCollectSuccess.show(); + } else { + $consentCollectSuccess.hide(); + } + + if (inProgress) { + $consentCollectInProgress.show(); + } else { + $consentCollectInProgress.hide(); + } + + if (notStarted) { + $consentCollectNotStarted.show(); + } else { + $consentCollectNotStarted.hide(); + } + } + +} ); diff --git a/assets/js/admin/whatsapp-disconnect.js b/assets/js/admin/whatsapp-disconnect.js new file mode 100644 index 000000000..91cbde3de --- /dev/null +++ b/assets/js/admin/whatsapp-disconnect.js @@ -0,0 +1,75 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + * @package FacebookCommerce + */ + +jQuery( document ).ready( function( $ ) { + // Get the modal and related elements + var modal = document.getElementById("wc-fb-disconnect-warning-modal"); + var cancelButton = document.getElementById("wc-fb-disconnect-warning-modal-cancel"); + var confirmButton = document.getElementById("wc-fb-disconnect-warning-modal-confirm"); + + // On click of the remove button, show the warning modal + $("#wc-whatsapp-disconnect-button").click(function(event) { + // Show the modal + modal.style.display = "block"; + + // Prevent default action + event.preventDefault(); + }); + + if (cancelButton) { + // Close modal when clicking the Cancel button + cancelButton.onclick = function() { + modal.style.display = "none"; + }; + } + + if (confirmButton) { + // Handle confirm action + confirmButton.onclick = function() { + $.post( facebook_for_woocommerce_whatsapp_disconnect.ajax_url, { + action: 'wc_facebook_disconnect_whatsapp', + nonce: facebook_for_woocommerce_whatsapp_disconnect.nonce + }, function ( response ) { + if ( response.success ) { + let url = new URL(window.location.href); + let params = new URLSearchParams(url.search); + params.delete('view'); + url.search = params.toString(); + window.location.href = url.toString(); + console.log( 'Whatsapp Disconnect Success', response ); + } else { + console.log("Whatsapp Disconnect Failure!!!",response); + } + } ); + + // Close the modal + modal.style.display = "none"; + }; + } + + // handle whatsapp disconnect widget edit link click should open business manager with whatsapp asset selected + $( '#wc-whatsapp-disconnect-edit' ).click( function( event ) { + $.post( facebook_for_woocommerce_whatsapp_disconnect.ajax_url, { + action: 'wc_facebook_whatsapp_fetch_url_info', + nonce: facebook_for_woocommerce_whatsapp_disconnect.nonce + }, function ( response ) { + + if ( response.success ) { + console.log( 'Whatsapp Edit Url Info Fetched Successfully', response ); + var business_id = response.data.business_id; + var asset_id = response.data.waba_id; + const WHATSAPP_MANAGER_URL = `https://business.facebook.com/latest/whatsapp_manager/phone_numbers/?asset_id=${asset_id}&business_id=${business_id}`; + window.open(WHATSAPP_MANAGER_URL); + } else { + console.log( 'Whatsapp Edit Url Info Fetch Failure', response ); + } + } ); + }); + +} ); diff --git a/assets/js/admin/whatsapp-events.js b/assets/js/admin/whatsapp-events.js new file mode 100644 index 000000000..e83dfa202 --- /dev/null +++ b/assets/js/admin/whatsapp-events.js @@ -0,0 +1,208 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + * @package FacebookCommerce + */ + +jQuery(document).ready(function ($) { + // Set Event Status for Order Placed + var orderPlacedActiveStatus = $('#order-placed-active-status'); + var orderPlacedInactiveStatus = $('#order-placed-inactive-status'); + if (facebook_for_woocommerce_whatsapp_events.order_placed_enabled) { + orderPlacedInactiveStatus.hide(); + orderPlacedActiveStatus.show(); + } + else { + orderPlacedActiveStatus.hide(); + orderPlacedInactiveStatus.show(); + } + + // Set Event Status for Order FulFilled + var orderFulfilledActiveStatus = $('#order-fulfilled-active-status'); + var orderFulfilledInactiveStatus = $('#order-fulfilled-inactive-status'); + if (facebook_for_woocommerce_whatsapp_events.order_fulfilled_enabled) { + orderFulfilledInactiveStatus.hide(); + orderFulfilledActiveStatus.show(); + } + else { + orderFulfilledActiveStatus.hide(); + orderFulfilledInactiveStatus.show(); + } + + // Set Event Status for Order Refunded + var orderRefundedActiveStatus = $('#order-refunded-active-status'); + var orderRefundedInactiveStatus = $('#order-refunded-inactive-status'); + if (facebook_for_woocommerce_whatsapp_events.order_refunded_enabled) { + orderRefundedInactiveStatus.hide(); + orderRefundedActiveStatus.show(); + } + else { + orderRefundedActiveStatus.hide(); + orderRefundedInactiveStatus.show(); + } + + $('#woocommerce-whatsapp-manage-order-placed, #woocommerce-whatsapp-manage-order-fulfilled, #woocommerce-whatsapp-manage-order-refunded').click(function (event) { + var clickedButtonId = $(event.target).attr("id"); + let view = clickedButtonId.replace("woocommerce-whatsapp-", ""); + view = view.replaceAll("-", "_"); + let url = new URL(window.location.href); + let params = new URLSearchParams(url.search); + params.set('view', view); + url.search = params.toString(); + window.location.href = url.toString(); + }); + + // call template library get API to show message template header, body and button text configured for the event. + $("#library-template-content").load(facebook_for_woocommerce_whatsapp_events.ajax_url, function () { + $.post(facebook_for_woocommerce_whatsapp_events.ajax_url, { + action: 'wc_facebook_whatsapp_fetch_library_template_info', + nonce: facebook_for_woocommerce_whatsapp_events.nonce, + event: facebook_for_woocommerce_whatsapp_events.event, + }, function (response) { + if (response.success) { + const event = facebook_for_woocommerce_whatsapp_events.event; + const headerReplacements = { + "ORDER_REFUNDED": { + "{{1}}": "{{$amount}}" + } + }; + const bodyReplacements = { + "ORDER_PLACED": { + "{{1}}": "{{first_name}}", + "{{2}}": "#{{order_number}}" + }, + "ORDER_FULFILLED": { + "{{1}}": "{{first_name}}", + "{{2}}": "#{{order_number}}" + }, + "ORDER_REFUNDED": { + "{{1}}": "{{first_name}}", + "{{2}}": "{{$amount}}", + "{{3}}": "#{{order_number}}" + } + }; + const parsedData = JSON.parse(response.data); + const apiResponseData = parsedData.data[0]; + // Parse template strings as HTML and extract text content to sanitize text + var header = $.parseHTML(apiResponseData.header)[0].textContent; + header = header.replace(/{{\d+}}/g, function (match) { + return headerReplacements[event][match]; + }); + var body = $.parseHTML(apiResponseData.body)[0].textContent; + body = body.replace(/{{\d+}}/g, function(match) { + return bodyReplacements[event][match]; + }); + // Body content has line breaks that need to be rendered in html + body = body.replace(/\n/g, '
'); + if (facebook_for_woocommerce_whatsapp_events.event === "ORDER_REFUNDED") { + $('#library-template-content').html(` +

Header

+

${header}

+

Body

+

${body}

+ `).show(); + } + else { + const button = $.parseHTML(apiResponseData.buttons[0].text)[0].textContent; + $('#library-template-content').html(` +

Header

+

${header}

+

Body

+

${body}

+

Call to action

+

${button}

+ `).show(); + } + console.log('Whatsapp Library Template call succeeded', response); + } + else { + console.log('Whatsapp Library Template call failed', response); + const message = facebook_for_woocommerce_whatsapp_finish.i18n.generic_error; + const errorNoticeHtml = ` +
+

${message}

+
+ `; + $('#events-error-notice').html(errorNoticeHtml).show(); + } + }); + }); + + $('#woocommerce-whatsapp-save-order-confirmation').click(function (event) { + var languageValue = $("#manage-event-language").val(); + var statusValue = $('input[name="template-status"]:checked').val(); + console.log('Save confirmation clicked: ', languageValue, statusValue); + $.post(facebook_for_woocommerce_whatsapp_events.ajax_url, { + action: 'wc_facebook_whatsapp_upsert_event_config', + nonce: facebook_for_woocommerce_whatsapp_events.nonce, + event: facebook_for_woocommerce_whatsapp_events.event, + language: languageValue, + status: statusValue + }, function (response) { + if (response.success) { + let url = new URL(window.location.href); + let params = new URLSearchParams(url.search); + params.set('view', 'utility_settings'); + url.search = params.toString(); + window.location.href = url.toString(); + console.log('Whatsapp Event Config has been updated', response); + } + else { + console.log('Whatsapp Event Config Update failure', response); + const message = facebook_for_woocommerce_whatsapp_finish.i18n.generic_error; + const errorNoticeHtml = ` +
+

${message}

+
+ `; + $('#events-error-notice').html(errorNoticeHtml).show(); + } + }); + }); + + $("#manage-event-language").load(facebook_for_woocommerce_whatsapp_events.ajax_url, function () { + $.post(facebook_for_woocommerce_whatsapp_events.ajax_url, { + action: 'wc_facebook_whatsapp_fetch_supported_languages', + nonce: facebook_for_woocommerce_whatsapp_events.nonce, + }, function (response) { + if (response.success) { + const parsedData = JSON.parse(response.data); + const supportedLanguages = parsedData.supported_languages; + $.each(supportedLanguages, function (index, languageObj) { + var displayValue = $.parseHTML(languageObj.display_value)[0].textContent; + var locale = $.parseHTML(languageObj.locale)[0].textContent; + $("#manage-event-language").append($("").text(displayValue).val(locale)); + }); + var eventConfiglanguage = getEventLanguage(facebook_for_woocommerce_whatsapp_events.event); + $("#manage-event-language").val(eventConfiglanguage); + console.log('Fetch supported language call succeeded'); + } + else { + console.log('Fetch supported language call failed', response); + const message = facebook_for_woocommerce_whatsapp_finish.i18n.generic_error; + const errorNoticeHtml = ` +
+

${message}

+
+ `; + $('#events-error-notice').html(errorNoticeHtml).show(); + } + }); + }); + + function getEventLanguage(event) { + switch (event) { + case "ORDER_PLACED": + return facebook_for_woocommerce_whatsapp_events.order_placed_language; + case "ORDER_FULFILLED": + return facebook_for_woocommerce_whatsapp_events.order_fulfilled_language; + case "ORDER_REFUNDED": + return facebook_for_woocommerce_whatsapp_events.order_refunded_language; + default: + return null; + } + } +}); \ No newline at end of file diff --git a/assets/js/admin/whatsapp-finish.js b/assets/js/admin/whatsapp-finish.js new file mode 100644 index 000000000..98d22e8ea --- /dev/null +++ b/assets/js/admin/whatsapp-finish.js @@ -0,0 +1,53 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + * @package FacebookCommerce + */ + +jQuery( document ).ready( function( $ ) { + // handle the whatsapp finish button click + $( '#wc-whatsapp-onboarding-finish' ).click( function( event ) { + // call the connect API to create configs and check payment + $.post( facebook_for_woocommerce_whatsapp_finish.ajax_url, { + action: 'wc_facebook_whatsapp_finish_onboarding', + nonce: facebook_for_woocommerce_whatsapp_finish.nonce + }, function ( response ) { + if ( response.success ) { + // If success, redirect to utility settings page + let url = new URL(window.location.href); + let params = new URLSearchParams(url.search); + params.set('view', 'utility_settings'); + url.search = params.toString(); + window.location.href = url.toString(); + console.log( 'Whatsapp Connect Success', response ); + } else { + var message; + const error = response.data; + console.log( 'Whatsapp Connect Failure', response ); + + switch (error) { + case "Incorrect payment setup": + message = facebook_for_woocommerce_whatsapp_finish.i18n.payment_setup_error; + break; + case "Onboarding is not complete or has failed.": + message = facebook_for_woocommerce_whatsapp_finish.i18n.onboarding_incomplete_error; + break; + default: + message = facebook_for_woocommerce_whatsapp_finish.i18n.generic_error; + } + + + const errorNoticeHtml = ` +
+

${message}

+
+ `; + $( '#payment-method-error-notice' ).html( errorNoticeHtml ).show(); + } + } ); + }); + +} ); diff --git a/assets/js/admin/whatsapp-templates.js b/assets/js/admin/whatsapp-templates.js new file mode 100644 index 000000000..5fd150242 --- /dev/null +++ b/assets/js/admin/whatsapp-templates.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + * @package FacebookCommerce + */ + +jQuery( document ).ready( function( $ ) { + // handle whatsapp view insights link click should open template insights in WhatsSpp Manager + $( '#woocommerce-whatsapp-manager-insights' ).click( function( event ) { + $.post( facebook_for_woocommerce_whatsapp_templates.ajax_url, { + action: 'wc_facebook_whatsapp_fetch_url_info', + nonce: facebook_for_woocommerce_whatsapp_templates.nonce + }, function ( response ) { + console.log(response); + if ( response.success ) { + console.log( 'Whatsapp Template Insights Info was fetched successfully', response ); + var business_id = response.data.business_id; + var asset_id = response.data.waba_id; + const MANAGE_TEMPLATES_URL = `https://business.facebook.com/latest/whatsapp_manager/message_templates?business_id=${business_id}&asset_id=${asset_id}`; + window.open(MANAGE_TEMPLATES_URL); + } + else { + console.log( 'Whatsapp Template Insights Info fetch call failed', response ); + } + } ); + }); +} ); diff --git a/changelog.txt b/changelog.txt index 4cbba4b58..d1a709b73 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,27 @@ *** Facebook for WooCommerce Changelog *** += 3.4.9 - 2025-05-14 = +* Add - Support for rollout switches in the plugin to control feature rollouts from meta side @francorisso in #3126 +* Fix - Tests in the rollout switches file by @francorisso in #3146 +* Fix - RolloutSwitches Init by @carterbuce in #3157 +* Add - Integrate Whatsapp Utility Messaging for WooCommerce Order Update Notifications by @sharunaanandraj in #3164 +* Tweak - Improve Test Filter Management with AbstractWPUnitTestWithSafeFiltering by @sol-loup in #2944 +* Fix - Namespacing issue causing some tests to be skipped @sol-loup in #3037 +* Tweak - Additional logs and timeout for Utility Message Flows by @woo-ardsouza in #3171 +* Fix - The WAUM payment progress to only Show Up after Consent Collection is Enabled by @sharunaanandraj in #3175 +* Tweak - Update language dropdown based on supported_languages in GET api response by @woo-ardsouza in #3178 +* Add - Error notice to gracefully handle errors in Manage Events view by @woo-ardsouza in #3179 +* Fix - The Status on the Whatsapp Consent Collection Pill and Button @sharunaanandraj in #3183 +* Tweak - Update Message Sending API from Messages to Message Events by @woo-ardsouza in #3182 +* Tweak - Update the Authentication mechanism for Whatsapp Webhook by @sharunaanandraj #3186 +* Tweak - Minor design updates to Utility Event Settings card by @woo-ardsouza in #3193 +* Add - Admin notice for WhatsApp utility messaging recruitment @iodic in #3177 +* Fix - The product sync button showing up twice by @sharunaanandraj in #3199 +* Tweak - Bump WooCommerce and WordPress compatibility by @iodic in #3200 +* Add - An automated process that synchronizes all WooCommerce product categories with Meta, creating catalog product sets for each category. The synchronization process ensures that any changes made to the WooCommerce product categories are reflected in the corresponding Meta catalog product sets by @mshymon in #3168 +* Add - A banner in product sets tab to explain recent changes to product sets sync by @mshymon in #3207 +* Add - Admin notice for WhatsApp utility messaging recruitment by @iodic in #3211 + = 3.4.8 - 2025-05-06 = * Add - Feature to sync global attributes to Meta and test API format by @devbodaghe in #3050 * Fix - Facebook attribute dropdown display and syncing issues by @devbodaghe in #3051 diff --git a/class-wc-facebookcommerce.php b/class-wc-facebookcommerce.php index 119b63133..9d30fa266 100644 --- a/class-wc-facebookcommerce.php +++ b/class-wc-facebookcommerce.php @@ -82,6 +82,9 @@ class WC_Facebookcommerce extends WooCommerce\Facebook\Framework\Plugin { private $sync_background_handler; /** @var WooCommerce\Facebook\ProductSets\Sync product sets sync handler */ + private $legacy_product_sets_sync_handler; + + /** @var WooCommerce\Facebook\ProductSets\ProductSetSync product sets sync handler */ private $product_sets_sync_handler; /** @var WooCommerce\Facebook\Handlers\Connection connection handler */ @@ -90,6 +93,9 @@ class WC_Facebookcommerce extends WooCommerce\Facebook\Framework\Plugin { /** @var WooCommerce\Facebook\Handlers\WebHook webhook handler */ private $webhook_handler; + /** @var WooCommerce\Facebook\Handlers\Whatsapp_WebHook whatsapp webhook handler */ + private $whatsapp_webhook_handler; + /** @var WooCommerce\Facebook\Commerce commerce handler */ private $commerce_handler; @@ -111,6 +117,9 @@ class WC_Facebookcommerce extends WooCommerce\Facebook\Framework\Plugin { /** @var WooCommerce\Facebook\Products\FBCategories instance. */ private $fb_categories; + /** @var WooCommerce\Facebook\RolloutSwitches instance. */ + private $rollout_switches; + /** * The Debug tools instance. * @@ -160,6 +169,7 @@ public function init() { add_action( 'init', array( $this, 'get_integration' ) ); add_action( 'init', array( $this, 'register_custom_taxonomy' ) ); add_action( 'add_meta_boxes_product', array( $this, 'remove_product_fb_product_set_metabox' ), 50 ); + add_action( 'woocommerce_init', array($this, 'add_whatsapp_consent_checkout_fields')); add_filter( 'fb_product_set_row_actions', array( $this, 'product_set_links' ) ); add_filter( 'manage_edit-fb_product_set_columns', array( $this, 'manage_fb_product_set_columns' ) ); @@ -183,16 +193,16 @@ public function init() { $this->heartbeat = new Heartbeat( WC()->queue() ); $this->heartbeat->init(); - - $this->product_feed = new WooCommerce\Facebook\Products\Feed(); - $this->products_stock_handler = new WooCommerce\Facebook\Products\Stock(); - $this->products_sync_handler = new WooCommerce\Facebook\Products\Sync(); - $this->sync_background_handler = new WooCommerce\Facebook\Products\Sync\Background(); - $this->configuration_detection = new WooCommerce\Facebook\Feed\FeedConfigurationDetection(); - $this->product_sets_sync_handler = new WooCommerce\Facebook\ProductSets\Sync(); - $this->commerce_handler = new WooCommerce\Facebook\Commerce(); - $this->fb_categories = new WooCommerce\Facebook\Products\FBCategories(); - $this->external_version_update = new WooCommerce\Facebook\ExternalVersionUpdate\Update(); + $this->product_feed = new WooCommerce\Facebook\Products\Feed(); + $this->products_stock_handler = new WooCommerce\Facebook\Products\Stock(); + $this->products_sync_handler = new WooCommerce\Facebook\Products\Sync(); + $this->sync_background_handler = new WooCommerce\Facebook\Products\Sync\Background(); + $this->configuration_detection = new WooCommerce\Facebook\Feed\FeedConfigurationDetection(); + $this->legacy_product_sets_sync_handler = new WooCommerce\Facebook\ProductSets\Sync(); + $this->product_sets_sync_handler = new WooCommerce\Facebook\ProductSets\ProductSetSync(); + $this->commerce_handler = new WooCommerce\Facebook\Commerce(); + $this->fb_categories = new WooCommerce\Facebook\Products\FBCategories(); + $this->external_version_update = new WooCommerce\Facebook\ExternalVersionUpdate\Update(); if ( wp_doing_ajax() ) { $this->ajax = new WooCommerce\Facebook\AJAX(); @@ -213,23 +223,28 @@ public function init() { $this->connection_handler = new WooCommerce\Facebook\Handlers\Connection( $this ); $this->webhook_handler = new WooCommerce\Facebook\Handlers\WebHook( $this ); + $this->whatsapp_webhook_handler = new WooCommerce\Facebook\Handlers\Whatsapp_Webhook( $this ); $this->tracker = new WooCommerce\Facebook\Utilities\Tracker(); + $this->rollout_switches = new WooCommerce\Facebook\RolloutSwitches( $this ); + // Init jobs $this->job_manager = new WooCommerce\Facebook\Jobs\JobManager(); add_action( 'init', [ $this->job_manager, 'init' ] ); + add_action( 'admin_init', [ $this->rollout_switches, 'init' ] ); // Instantiate the debug tools. $this->debug_tools = new DebugTools(); // load admin handlers, before admin_init if ( is_admin() ) { - $this->admin_settings = new WooCommerce\Facebook\Admin\Settings( $this->connection_handler->is_connected() ); + $this->admin_settings = new WooCommerce\Facebook\Admin\Settings( $this ); } } } + /** * Initializes the admin handling. * @@ -588,6 +603,17 @@ public function get_products_sync_handler() { return $this->products_sync_handler; } + /** + * Gets the products sync handler. + * + * @since 3.4.9 + * + * @return WooCommerce\Facebook\ProductSets\ProductSetSync + */ + public function get_product_sets_sync_handler() { + return $this->product_sets_sync_handler; + } + /** * Gets the products sync background handler. @@ -770,6 +796,15 @@ public function get_asset_build_dir_url() { return $this->get_plugin_url() . '/assets/build'; } + /** + * Gets the connection handler. + * + * @return WooCommerce\Facebook\RolloutSwitches + */ + public function get_rollout_switches() { + return $this->rollout_switches; + } + /** Conditional methods ***************************************************************************************/ @@ -843,6 +878,30 @@ protected function get_current_page_id() { } return $current_screen_id; } + + /** + * Add checkout fields to collect whatsapp consent if consent collection is enabled + * + * @since 2.3.0 + * + * @param array $fields + * + * @return array + */ + function add_whatsapp_consent_checkout_fields($fields) { + if (get_option('wc_facebook_whatsapp_consent_collection_setting_status', 'disabled') === 'enabled') { + woocommerce_register_additional_checkout_field( + array( + 'id' => 'wc_facebook/whatsapp_consent_checkbox', // id = namespace/field_name + 'label' => esc_html('Get order updates on WhatsApp'), + 'location' => 'address', + 'type' => 'checkbox', + 'optionalLabel' => esc_html('Get order updates on WhatsApp') + ) + ); + } + return $fields; + } } diff --git a/facebook-commerce-admin-banner.php b/facebook-commerce-admin-banner.php new file mode 100644 index 000000000..d134cf561 --- /dev/null +++ b/facebook-commerce-admin-banner.php @@ -0,0 +1,128 @@ + admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( self::BANNER_ID ), + 'banner_id' => self::BANNER_ID, + ) + ); + } + + /** + * AJAX handler to dismiss the banner. + */ + public function ajax_dismiss_banner() { + check_ajax_referer( self::BANNER_ID, 'nonce' ); + update_user_meta( + get_current_user_id(), + self::BANNER_ID, + 1 + ); + } + + /** + * Output the banner HTML if it should be shown. + */ + public function render_banner() { + // Check if the WhatsApp admin banner should be shown. + if ( strtotime( 'now' ) > strtotime( '2025-06-15 23:59:59' ) ) { + return; + } + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return; + } + + if ( get_user_meta( + get_current_user_id(), + self::BANNER_ID, + true + ) ) { + return; + } + + $banner_html = '
'; + $banner_html .= 'WhatsApp Logo'; + $banner_html .= '

Sign up to test WhatsApp’s new integration with ' + . 'WooCommerce

'; + $banner_html .= '

We’re launching a brand new WhatsApp integration for ' + . 'WooCommerce allowing businesses to send order tracking notifications ' + . 'on WhatsApp. Sign up for a chance to join our testing program and get ' + . 'early access to this new feature. As a thank you, participants who ' + . 'complete testing will receive a $500 ad credit.

'; + $banner_html .= 'Sign Up'; + $banner_html .= 'Close button'; + $banner_html .= '
'; + + echo wp_kses_post( $banner_html ); + } +} diff --git a/facebook-commerce-admin-notice.php b/facebook-commerce-admin-notice.php new file mode 100644 index 000000000..25a589b3f --- /dev/null +++ b/facebook-commerce-admin-notice.php @@ -0,0 +1,106 @@ + admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( self::NOTICE_ID ), + 'notice_id' => self::NOTICE_ID, + ) + ); + } + + /** + * Handles the AJAX request to dismiss the notice. + */ + public function ajax_dismiss_notice() { + check_ajax_referer( self::NOTICE_ID, 'nonce' ); + update_user_meta( get_current_user_id(), self::NOTICE_ID, 1 ); + wp_send_json_success(); + } + + /** + * Displays the admin notice if not dismissed. + */ + public function show_notice() { + if ( strtotime( 'now' ) > strtotime( '2025-06-16 23:59:59' ) ) { + return; + } + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return; + } + + if ( get_user_meta( get_current_user_id(), self::NOTICE_ID, true ) ) { + return; + } + + $dismiss_url = add_query_arg( + array( + self::NOTICE_ID => '1', + '_wpnonce' => wp_create_nonce( self::NOTICE_ID ), + ) + ); + + ?> + +
+

+ Sign up for our testing program and get early access now!", + 'facebook-for-woocommerce' + ), + array( + 'a' => array( + 'href' => array(), + ), + ) + ), + 'https://facebookpso.qualtrics.com/jfe/form/SV_0SVseus9UADOhhQ' + ); + ?> +

+
+ 'ORDER_PLACED', + 'completed' => 'ORDER_FULFILLED', + 'refunded' => 'ORDER_REFUNDED', + ); + + public function __construct() { + if ( ! $this->is_whatsapp_utility_enabled() ) { + return; + } + add_action( 'woocommerce_order_status_changed', array( $this, 'process_wc_order_status_changed' ), 10, 3 ); + } + + /** + * Determines if WhatsApp Utility Messages are enabled + * TODO: Update this function to check for gating logic for Alpha businesses + * + * @since 2.3.0 + * + * @return bool + */ + private function is_whatsapp_utility_enabled() { + return true; + } + + + /** + * Hook to process Order Processing, Order Completed and Order Refunded events for WhatsApp Utility Messages + * + * @param string $order_id Order id + * @param string $old_status Old Order Status + * @param string $new_status New Order Status + * + * @return void + * @since 2.3.0 + */ + public function process_wc_order_status_changed( $order_id, $old_status, $new_status ) { + // WhatsApp Utility Messages are supported only for Processing status + $supported_statuses = array_keys( self::ORDER_STATUS_TO_EVENT_MAPPING ); + if ( ! in_array( $new_status, $supported_statuses, true ) ) { + return; + } + + wc_get_logger()->info( + sprintf( + /* translators: %s $order_id */ + __( 'Processing Order id %1$s to send Whatsapp Utility messages', 'facebook-for-woocommerce' ), + $order_id, + ) + ); + $event = self::ORDER_STATUS_TO_EVENT_MAPPING[ $new_status ]; + + // Check WhatsApp Event Config is active + $event_config_id_option_name = implode( '_', array( WhatsAppUtilityConnection::WA_UTILITY_OPTION_PREFIX, strtolower( $event ), 'event_config_id' ) ); + $event_config_language_option_name = implode( '_', array( WhatsAppUtilityConnection::WA_UTILITY_OPTION_PREFIX, strtolower( $event ), 'language' ) ); + $event_config_id = get_option( $event_config_id_option_name, null ); + $language_code = get_option( $event_config_language_option_name, null ); + if ( empty( $event_config_id ) || empty( $language_code ) ) { + wc_get_logger()->info( + sprintf( + /* translators: %s $order_id */ + __( 'Messages Post API call for Order id %1$s skipped due to no active event config', 'facebook-for-woocommerce' ), + $order_id, + ) + ); + return; + } + + $order = wc_get_order( $order_id ); + // Check WhatsApp Consent Checkbox is selected in shipping and billing + $billing_consent_value = $order->get_meta( '_wc_billing/wc_facebook/whatsapp_consent_checkbox' ); + $shipping_consent_value = $order->get_meta( '_wc_shipping/wc_facebook/whatsapp_consent_checkbox' ); + $has_whatsapp_consent = $billing_consent_value && $shipping_consent_value; + // Get WhatsApp Phone number from entered Billing and Shipping phone number + $billing_phone_number = $order->get_billing_phone(); + $shipping_phone_number = $order->get_shipping_phone(); + $phone_number = ( isset( $billing_phone_number ) && $billing_consent_value ) ? $billing_phone_number : $shipping_phone_number; + // Get Customer first name + $first_name = $order->get_billing_first_name(); + // Get Total Refund Amount for Order Refunded event + $total_refund = 0; + foreach ( $order->get_refunds() as $refund ) { + $total_refund += $refund->get_amount(); + } + $currency = $order->get_currency(); + $refund_amount = $total_refund * 1000; + if ( empty( $phone_number ) || ! $has_whatsapp_consent || empty( $event ) || empty( $first_name ) ) { + wc_get_logger()->info( + sprintf( + /* translators: %s $order_id */ + __( 'Messages Post API call for Order id %1$s skipped due to missing whatsapp consent or Order info', 'facebook-for-woocommerce' ), + $order_id, + ) + ); + return; + } + + // Check Access token and WACS is available + $bisu_token = get_option( 'wc_facebook_wa_integration_bisu_access_token', null ); + $wacs_id = get_option( 'wc_facebook_wa_integration_wacs_id', null ); + if ( empty( $bisu_token ) || empty( $wacs_id ) ) { + wc_get_logger()->info( + sprintf( + /* translators: %s $order_id */ + __( 'Messages Post API call for Order id %1$s Failed due to missing access token or wacs info', 'facebook-for-woocommerce' ), + $order_id, + ) + ); + return; + } + WhatsAppUtilityConnection::post_whatsapp_utility_messages_events_call( $event, $event_config_id, $language_code, $wacs_id, $order_id, $phone_number, $first_name, $refund_amount, $currency, $bisu_token ); + } +} diff --git a/facebook-commerce.php b/facebook-commerce.php index be88640c3..3fa6ce39b 100644 --- a/facebook-commerce.php +++ b/facebook-commerce.php @@ -23,6 +23,9 @@ require_once 'facebook-config-warmer.php'; require_once 'includes/fbproduct.php'; require_once 'facebook-commerce-pixel-event.php'; +require_once 'facebook-commerce-admin-notice.php'; + +new WC_Facebookcommerce_Admin_Notice(); class WC_Facebookcommerce_Integration extends WC_Integration { @@ -173,6 +176,9 @@ class WC_Facebookcommerce_Integration extends WC_Integration { /** @var WC_Facebook_Product_Feed instance. */ private $fbproductfeed; + /** @var WC_Facebookcommerce_Whatsapp_Utility_Event instance. */ + private $wa_utility_event_processor; + /** * Init and hook in the integration. * @@ -360,6 +366,9 @@ public function __construct( WC_Facebookcommerce $facebook_for_woocommerce ) { // Product Set hooks. add_action( 'fb_wc_product_set_sync', [ $this, 'create_or_update_product_set_item' ], 99, 2 ); add_action( 'fb_wc_product_set_delete', [ $this, 'delete_product_set_item' ], 99 ); + + // Init Whatsapp Utility Event Processor + $this->wa_utility_event_processor = $this->load_whatsapp_utility_event_processor(); } /** @@ -849,27 +858,27 @@ private function save_facebook_product_attributes( $woo_product ) { if ( isset( $_POST[ WC_Facebook_Product::FB_SIZE ] ) ) { $woo_product->set_fb_size( sanitize_text_field( wp_unslash( $_POST[ WC_Facebook_Product::FB_SIZE ] ) ) ); } - + if ( isset( $_POST[ WC_Facebook_Product::FB_COLOR ] ) ) { $woo_product->set_fb_color( sanitize_text_field( wp_unslash( $_POST[ WC_Facebook_Product::FB_COLOR ] ) ) ); } - + if ( isset( $_POST[ WC_Facebook_Product::FB_MATERIAL ] ) ) { $woo_product->set_fb_material( sanitize_text_field( wp_unslash( $_POST[ WC_Facebook_Product::FB_MATERIAL ] ) ) ); } - + if ( isset( $_POST[ WC_Facebook_Product::FB_PATTERN ] ) ) { $woo_product->set_fb_pattern( sanitize_text_field( wp_unslash( $_POST[ WC_Facebook_Product::FB_PATTERN ] ) ) ); } - + if ( isset( $_POST[ WC_Facebook_Product::FB_AGE_GROUP ] ) ) { $woo_product->set_fb_age_group( sanitize_text_field( wp_unslash( $_POST[ WC_Facebook_Product::FB_AGE_GROUP ] ) ) ); } - + if ( isset( $_POST[ WC_Facebook_Product::FB_GENDER ] ) ) { $woo_product->set_fb_gender( sanitize_text_field( wp_unslash( $_POST[ WC_Facebook_Product::FB_GENDER ] ) ) ); } - + if ( isset( $_POST[ WC_Facebook_Product::FB_PRODUCT_CONDITION ] ) ) { $woo_product->set_fb_condition( sanitize_text_field( wp_unslash( $_POST[ WC_Facebook_Product::FB_PRODUCT_CONDITION ] ) ) ); } @@ -901,7 +910,7 @@ private function save_product_settings( WC_Product $product ) { $woo_product->set_description( sanitize_text_field( wp_unslash( $_POST[ self::FB_PRODUCT_DESCRIPTION ] ) ) ); $woo_product->set_rich_text_description( $_POST[ self::FB_PRODUCT_DESCRIPTION ] ); } - + if ( isset( $_POST[ WC_Facebook_Product::FB_PRODUCT_PRICE ] ) ) { $woo_product->set_price( sanitize_text_field( wp_unslash( $_POST[ WC_Facebook_Product::FB_PRODUCT_PRICE ] ) ) ); } @@ -3027,4 +3036,19 @@ public function ajax_display_test_result() { wp_die(); } + /** + * Init WhatsApp Utility Event Processor. + * + * @return void + */ + public function load_whatsapp_utility_event_processor() { + // Attempt to load WhatsApp Utility Event Processor + include_once 'facebook-commerce-whatsapp-utility-event.php'; + if ( class_exists( 'WC_Facebookcommerce_Whatsapp_Utility_Event' ) ) { + if ( ! isset( $this->wa_utility_event_processor ) ) { + $this->wa_utility_event_processor = new WC_Facebookcommerce_Whatsapp_Utility_Event( $this ); + } + } + } + } diff --git a/facebook-for-woocommerce.php b/facebook-for-woocommerce.php index 24f73984b..e66c3d9a7 100644 --- a/facebook-for-woocommerce.php +++ b/facebook-for-woocommerce.php @@ -10,14 +10,14 @@ * Description: Grow your business on Facebook! Use this official plugin to help sell more of your products using Facebook. After completing the setup, you'll be ready to create ads that promote your products and you can also create a shop section on your Page where customers can browse your products on Facebook. * Author: Facebook * Author URI: https://www.facebook.com/ - * Version: 3.4.8 + * Version: 3.4.9 * Requires at least: 5.6 * Requires PHP: 7.4 * Text Domain: facebook-for-woocommerce * Requires Plugins: woocommerce - * Tested up to: 6.7 + * Tested up to: 6.8.1 * WC requires at least: 6.4 - * WC tested up to: 9.6 + * WC tested up to: 9.8.5 * * @package FacebookCommerce */ @@ -38,6 +38,20 @@ function () { } } ); + +if ( is_admin() ) { + add_action( + 'admin_init', + function () { + if ( ! class_exists( 'WC_Facebookcommerce_Admin_Banner' ) ) { + require_once plugin_dir_path( __FILE__ ) . + 'facebook-commerce-admin-banner.php'; + } + new WC_Facebookcommerce_Admin_Banner(); + } + ); +} + /** * The plugin loader class. * @@ -48,7 +62,7 @@ class WC_Facebook_Loader { /** * @var string the plugin version. This must be in the main plugin file to be automatically bumped by Woorelease. */ - const PLUGIN_VERSION = '3.4.8'; // WRCS: DEFINED_VERSION. + const PLUGIN_VERSION = '3.4.9'; // WRCS: DEFINED_VERSION. // Minimum PHP version required by this plugin. const MINIMUM_PHP_VERSION = '7.4.0'; diff --git a/includes/AJAX.php b/includes/AJAX.php index b92243f1d..1f55794f5 100644 --- a/includes/AJAX.php +++ b/includes/AJAX.php @@ -14,6 +14,7 @@ use WooCommerce\Facebook\Framework\Helper; use WooCommerce\Facebook\Admin\Settings_Screens\Product_Sync; use WooCommerce\Facebook\Framework\Plugin\Exception as PluginException; +use WooCommerce\Facebook\Handlers\WhatsAppUtilityConnection; defined( 'ABSPATH' ) or exit; @@ -46,8 +47,35 @@ public function __construct() { // get the current sync status add_action( 'wp_ajax_wc_facebook_get_sync_status', array( $this, 'get_sync_status' ) ); + // check the status of whatsapp onboarding and update the progress + add_action( 'wp_ajax_wc_facebook_whatsapp_onboarding_progress_check', array( $this, 'whatsapp_onboarding_progress_check' ) ); + + // update the wp_options with wc_facebook_whatsapp_consent_collection_setting_status to enabled + add_action( 'wp_ajax_wc_facebook_whatsapp_consent_collection_enable', array( $this, 'whatsapp_consent_collection_enable' ) ); + + // fetch url info - waba id and business id + add_action( 'wp_ajax_wc_facebook_whatsapp_fetch_url_info', array( $this, 'wc_facebook_whatsapp_fetch_url_info' ) ); + + // action to fetch required info and make api call to meta to finish onboarding + add_action( 'wp_ajax_wc_facebook_whatsapp_finish_onboarding', array( $this, 'wc_facebook_whatsapp_finish_onboarding' ) ); + + // fetch configured library template info + add_action( 'wp_ajax_wc_facebook_whatsapp_fetch_library_template_info', array( $this, 'whatsapp_fetch_library_template_info' ) ); + + // action to create or update utility event config info + add_action( 'wp_ajax_wc_facebook_whatsapp_upsert_event_config', array( $this, 'whatsapp_upsert_event_config' ) ); + // search a product's attributes for the given term add_action( 'wp_ajax_' . self::ACTION_SEARCH_PRODUCT_ATTRIBUTES, array( $this, 'admin_search_product_attributes' ) ); + + // update the wp_options with wc_facebook_whatsapp_consent_collection_setting_status to disabled + add_action( 'wp_ajax_wc_facebook_whatsapp_consent_collection_disable', array( $this, 'whatsapp_consent_collection_disable' ) ); + + // disconnect whatsapp account from woocommcerce app + add_action( 'wp_ajax_wc_facebook_disconnect_whatsapp', array( $this, 'wc_facebook_disconnect_whatsapp' ) ); + + // get supported languages for whatsapp templates + add_action( 'wp_ajax_wc_facebook_whatsapp_fetch_supported_languages', array( $this, 'whatsapp_fetch_supported_languages' ) ); } @@ -156,6 +184,269 @@ public function get_sync_status() { wp_send_json_success( $remaining_products ); } + /** + * Get data for creating the billing or whatsapp manager url for whatsapp account. + * + * @internal + * + * @since 1.10.0 + */ + public function wc_facebook_whatsapp_fetch_url_info() { + wc_get_logger()->info( + sprintf( + __( 'Fetching url info(WABA ID+BusinessID) for whatsapp pages', 'facebook-for-woocommerce' ) + ) + ); + facebook_for_woocommerce()->log( '' ); + if ( ! check_ajax_referer( 'facebook-for-wc-whatsapp-billing-nonce', 'nonce', false ) && ! check_ajax_referer( 'facebook-for-wc-whatsapp-templates-nonce', 'nonce', false ) && ! check_ajax_referer( 'facebook-for-wc-whatsapp-disconnect-nonce', 'nonce', false ) ) { + wc_get_logger()->info( + sprintf( + __( 'Nonce Verification Error while Fetching Url Info', 'facebook-for-woocommerce' ) + ) + ); + wp_send_json_error( 'Invalid security token sent.' ); + } + + $waba_id = get_option( 'wc_facebook_wa_integration_waba_id', null ); + $business_id = get_option( 'wc_facebook_wa_integration_business_id', null ); + + if ( empty( $waba_id ) || empty( $business_id ) ) { + wc_get_logger()->info( + sprintf( + __( 'Missing Waba ID + Business ID during Fetch Url Info. Whatsapp Onboarding is not complete or has failed.', 'facebook-for-woocommerce' ) + ) + ); + wp_send_json_error( 'Whatsapp onboarding is not complete or has failed.' ); + } + + $response = array( + 'waba_id' => $waba_id, + 'business_id' => $business_id, + ); + + wp_send_json_success( $response ); + } + + /** + * Get data for for finish onboarding call and make api call. + * + * @internal + * + * @since 1.10.0 + */ + public function wc_facebook_whatsapp_finish_onboarding() { + wc_get_logger()->info( + sprintf( + __( 'Getting data for Whatsapp Finish Onboarding Done Button Click', 'facebook-for-woocommerce' ) + ) + ); + if ( ! check_ajax_referer( 'facebook-for-wc-whatsapp-finish-nonce', 'nonce', false ) ) { + wc_get_logger()->info( + sprintf( + __( 'Nonce Verification Error in Finish Onboarding Flow', 'facebook-for-woocommerce' ) + ) + ); + wp_send_json_error( 'Invalid security token sent.' ); + } + $external_business_id = get_option( 'wc_facebook_external_business_id', null ); + $wacs_id = get_option( 'wc_facebook_wa_integration_wacs_id', null ); + $waba_id = get_option( 'wc_facebook_wa_integration_waba_id', null ); + $bisu_token = get_option( 'wc_facebook_wa_integration_bisu_access_token', null ); + if ( empty( $external_business_id ) || empty( $wacs_id ) || empty( $waba_id ) || empty( $bisu_token ) ) { + wc_get_logger()->info( + sprintf( + __( 'Finish Onboarding - Onboarding is not complete or has failed.', 'facebook-for-woocommerce' ), + ) + ); + wp_send_json_error( 'Onboarding Flow is not complete or has failed.' ); + } + WhatsAppUtilityConnection::wc_facebook_whatsapp_connect_utility_messages_call( $waba_id, $wacs_id, $external_business_id, $bisu_token ); + } + + + /** + * Checks if the onboarding for whatsapp is complete once business has initiated onboarding. + * + * @internal + * + * @since 1.10.0 + */ + public function whatsapp_onboarding_progress_check() { + if ( ! check_ajax_referer( 'facebook-for-wc-whatsapp-onboarding-progress-nonce', 'nonce', false ) ) { + wp_send_json_error( 'Invalid security token sent.' ); + } + $waba_id = get_option( 'wc_facebook_wa_integration_waba_id', null ); + $is_payment_setup = (bool) get_option( 'wc_facebook_wa_integration_is_payment_setup', null ); + if ( ! empty( $waba_id ) ) { + wp_send_json_success( + array( + 'message' => 'WhatsApp onboarding is complete', + 'is_payment_setup' => $is_payment_setup, + ) + ); + } + wp_send_json_error( 'WhatsApp onboarding is not complete' ); + } + + public function whatsapp_consent_collection_enable() { + wc_get_logger()->info( + sprintf( + __( 'Enabling Whatsapp Consent Collection in Checkout Flow', 'facebook-for-woocommerce' ) + ) + ); + if ( ! check_ajax_referer( 'facebook-for-wc-whatsapp-consent-nonce', 'nonce', false ) ) { + wc_get_logger()->info( + sprintf( + __( 'Nonce Verification Error in Whatsapp Consent Collection', 'facebook-for-woocommerce' ) + ) + ); + wp_send_json_error( 'Invalid security token sent.' ); + } + if ( get_option( 'wc_facebook_whatsapp_consent_collection_setting_status' ) !== 'enabled' ) { + update_option( 'wc_facebook_whatsapp_consent_collection_setting_status', 'enabled' ); + } + $is_payment_setup = (bool) get_option( 'wc_facebook_wa_integration_is_payment_setup', null ); + wc_get_logger()->info( + sprintf( + __( 'Whatsapp Consent Collection Enabled Successfully in Checkout Flow', 'facebook-for-woocommerce' ) + ) + ); + wp_send_json_success( + array( + 'message' => 'Whatsapp Consent Collection Enabled Successfully in Checkout Flow', + 'is_payment_setup' => $is_payment_setup, + ) + ); + } + + public function whatsapp_consent_collection_disable() { + wc_get_logger()->info( + sprintf( + __( 'Disabling Whatsapp Consent Collection in Utility Settings View', 'facebook-for-woocommerce' ) + ) + ); + if ( ! check_ajax_referer( 'facebook-for-wc-whatsapp-consent-disable-nonce', 'nonce', false ) ) { + wp_send_json_error( 'Invalid security token sent.' ); + } + if ( get_option( 'wc_facebook_whatsapp_consent_collection_setting_status' ) !== 'disabled' ) { + update_option( 'wc_facebook_whatsapp_consent_collection_setting_status', 'disabled' ); + } + wc_get_logger()->info( + sprintf( + __( 'Whatsapp Consent Collection Disabled Successfully in Utility Settings View', 'facebook-for-woocommerce' ) + ) + ); + wp_send_json_success(); + } + + /** + * Disconnect Whatsapp from WooCommerce. + * + * @internal + * + * @since 1.10.0 + */ + public function wc_facebook_disconnect_whatsapp() { + wc_get_logger()->info( + sprintf( + __( 'Diconnecting Whatsapp From Woocommerce', 'facebook-for-woocommerce' ) + ) + ); + if ( ! check_ajax_referer( 'facebook-for-wc-whatsapp-disconnect-nonce', 'nonce', false ) ) { + wc_get_logger()->info( + sprintf( + __( 'Nonce Verification Failed while Diconnecting Whatsapp From Woocommerce', 'facebook-for-woocommerce' ) + ) + ); + wp_send_json_error( 'Invalid security token sent.' ); + } + + $integration_config_id = get_option( 'wc_facebook_wa_integration_config_id', null ); + $bisu_token = get_option( 'wc_facebook_wa_integration_bisu_access_token', null ); + $waba_id = get_option( 'wc_facebook_wa_integration_waba_id', null ); + if ( empty( $integration_config_id ) || empty( $bisu_token ) || empty( $waba_id ) ) { + wc_get_logger()->info( + sprintf( + __( 'Missing Integration Config ID, BISU token, WABA ID while Diconnecting Whatsapp From Woocommerce', 'facebook-for-woocommerce' ) + ) + ); + wp_send_json_error( 'Missing integration_config_id or bisu_token or waba_id for Disconnect API call' ); + } + WhatsAppUtilityConnection::wc_facebook_disconnect_whatsapp( $waba_id, $integration_config_id, $bisu_token ); + } + + public function whatsapp_fetch_library_template_info() { + facebook_for_woocommerce()->log( 'Fetching library template data for whatsapp utility event' ); + if ( ! check_ajax_referer( 'facebook-for-wc-whatsapp-events-nonce', 'nonce', false ) ) { + wp_send_json_error( 'Invalid security token sent.' ); + } + $bisu_token = get_option( 'wc_facebook_wa_integration_bisu_access_token', null ); + if ( empty( $bisu_token ) ) { + wp_send_json_error( 'Missing access token for Library template API call' ); + } + // Get POST parameters from the request + $event = isset( $_POST['event'] ) ? wc_clean( wp_unslash( $_POST['event'] ) ) : ''; + WhatsAppUtilityConnection::get_template_library_content( $event, $bisu_token ); + } + + public function whatsapp_fetch_supported_languages() { + wc_get_logger()->info( + sprintf( + __( 'Fetching supported languages for WhatsApp Utility Templates', 'facebook-for-woocommerce' ) + ) + ); + if ( ! check_ajax_referer( 'facebook-for-wc-whatsapp-events-nonce', 'nonce', false ) ) { + wc_get_logger()->info( + sprintf( + __( 'Nonce Verification Failed while fetching supported languages for WhatsApp Utility Templates', 'facebook-for-woocommerce' ) + ) + ); + wp_send_json_error( 'Invalid security token sent.' ); + } + $bisu_token = get_option( 'wc_facebook_wa_integration_bisu_access_token', null ); + $integration_config_id = get_option( 'wc_facebook_wa_integration_config_id', null ); + if ( empty( $bisu_token ) || empty( $integration_config_id ) ) { + wc_get_logger()->info( + sprintf( + __( 'Missing Integration Config ID, BISU token, WABA ID for Integration Config Get API call', 'facebook-for-woocommerce' ) + ) + ); + wp_send_json_error( 'Missing integration_config_id or bisu_token for Integration Config Get API call', 'facebook-for-woocommerce' ); + } + WhatsAppUtilityConnection::get_supported_languages_for_templates( $integration_config_id, $bisu_token ); + } + + /** + * Creates or Updates WhatsApp Utility Event Configs + * + * @internal + * + * @since 1.10.0 + */ + public function whatsapp_upsert_event_config() { + facebook_for_woocommerce()->log( 'Calling POST API to upsert whatsapp utility event' ); + if ( ! check_ajax_referer( 'facebook-for-wc-whatsapp-events-nonce', 'nonce', false ) ) { + wp_send_json_error( 'Invalid security token sent.' ); + } + // Get BISU token + $bisu_token = get_option( 'wc_facebook_wa_integration_bisu_access_token', null ); + if ( empty( $bisu_token ) ) { + wp_send_json_error( 'Missing access token for Event Configs POST API call' ); + } + // Get Integration Config id + $integration_config_id = get_option( 'wc_facebook_wa_integration_config_id', null ); + if ( empty( $integration_config_id ) ) { + wp_send_json_error( 'Missing Integration Config for Event Configs POST API call' ); + } + // Get POST parameters from the request + $event = isset( $_POST['event'] ) ? wc_clean( wp_unslash( $_POST['event'] ) ) : ''; + $language = isset( $_POST['language'] ) ? wc_clean( wp_unslash( $_POST['language'] ) ) : ''; + $status = isset( $_POST['status'] ) ? wc_clean( wp_unslash( $_POST['status'] ) ) : ''; + if ( empty( $event ) || empty( $language ) || empty( $status ) ) { + wp_send_json_error( 'Missing request parameters for Event Configs POST API call' ); + } + WhatsAppUtilityConnection::post_whatsapp_utility_messages_event_configs_call( $event, $integration_config_id, $language, $status, $bisu_token ); + } /** * Maybe triggers a modal warning when the merchant toggles sync enabled status in bulk. diff --git a/includes/API.php b/includes/API.php index cc8d4dc94..ad7f67da7 100644 --- a/includes/API.php +++ b/includes/API.php @@ -278,6 +278,29 @@ public function get_business_configuration( $external_business_id ) { return $this->perform_request( $request ); } + /** + * Gets rollout switches + * + * @param string $external_business_id + * @return API\FBE\RolloutSwitches\Response + * @throws ApiException + */ + public function get_rollout_switches( string $external_business_id ) { + if(!$this->get_access_token()) { + return null; + } + + $request = new API\FBE\RolloutSwitches\Request( $external_business_id ); + $request->set_params( + array( + 'access_token' => $this->get_access_token(), + 'fbe_external_business_id'=> $external_business_id + ) + ); + $this->set_response_handler( API\FBE\RolloutSwitches\Response::class ); + return $this->perform_request( $request ); + } + /** * Updates the plugin version configuration. * @@ -496,6 +519,19 @@ public function delete_product_set_item( string $product_set_id, bool $allow_liv return $this->perform_request( $request ); } + /** + * @param string $product_catalog_id + * @param array $data + * @return API\Response|API\ProductCatalog\ProductSets\Read\Response + * @throws ApiException + * @throws API\Exceptions\Request_Limit_Reached + */ + public function read_product_set_item( string $product_catalog_id, string $retailer_id ): API\ProductCatalog\ProductSets\Read\Response { + $request = new API\ProductCatalog\ProductSets\Read\Request( $product_catalog_id, $retailer_id ); + $this->set_response_handler( API\ProductCatalog\ProductSets\Read\Response::class ); + return $this->perform_request( $request ); + } + /** * @param string $product_catalog_id * @return API\Response|API\ProductCatalog\ProductFeeds\ReadAll\Response diff --git a/includes/API/FBE/RolloutSwitches/Request.php b/includes/API/FBE/RolloutSwitches/Request.php new file mode 100644 index 000000000..be72f2391 --- /dev/null +++ b/includes/API/FBE/RolloutSwitches/Request.php @@ -0,0 +1,20 @@ +response_data['data'] ?? []; + } +} diff --git a/includes/API/ProductCatalog/ProductSets/Read/Request.php b/includes/API/ProductCatalog/ProductSets/Read/Request.php new file mode 100644 index 000000000..349557bc0 --- /dev/null +++ b/includes/API/ProductCatalog/ProductSets/Read/Request.php @@ -0,0 +1,28 @@ + Product Sets > Get Graph Api. + * + * @link https://developers.facebook.com/docs/marketing-api/reference/product-catalog/product_sets/ + */ +class Request extends ApiRequest { + + /** + * @param string $product_catalog_id Facebook Product Catalog ID. + * @param string $retailer_id Facebook Product Set Retailer ID. + */ + public function __construct( string $product_catalog_id, string $retailer_id ) { + parent::__construct( "/{$product_catalog_id}/product_sets", 'GET' ); + parent::set_params( + array( 'retailer_id' => $retailer_id ) + ); + } +} diff --git a/includes/API/ProductCatalog/ProductSets/Read/Response.php b/includes/API/ProductCatalog/ProductSets/Read/Response.php new file mode 100644 index 000000000..88be20974 --- /dev/null +++ b/includes/API/ProductCatalog/ProductSets/Read/Response.php @@ -0,0 +1,29 @@ + Product Groups > Get Graph Api. + * + * @link https://developers.facebook.com/docs/marketing-api/reference/product-catalog/product_sets/ + * @property-read string id Facebook Product Set ID. + * + * @since 3.4.9 + */ +class Response extends ApiResponse { + + /** + * Returns the fb product set ID. + * + * @return ?string + * @since 3.4.9 + */ + public function get_product_set_id(): ?string { + return $this->data[0]['id'] ?? null; + } +} diff --git a/includes/Admin/Product_Sets.php b/includes/Admin/Product_Sets.php index e829d26f2..65cb1a000 100644 --- a/includes/Admin/Product_Sets.php +++ b/includes/Admin/Product_Sets.php @@ -13,6 +13,7 @@ defined( 'ABSPATH' ) || exit; use WP_Term; +use WooCommerce\Facebook\RolloutSwitches; /** * General handler for the product set admin functionality. @@ -65,8 +66,30 @@ public function __construct() { // save custom field data add_action( 'created_fb_product_set', array( $this, 'save_custom_field' ), 10, 2 ); add_action( 'edited_fb_product_set', array( $this, 'save_custom_field' ), 10, 2 ); + // show a banner about chnages to product sets sync + add_action( 'admin_notices', array( $this, 'display_fb_product_sets_banner' ) ); } + public function display_fb_product_sets_banner() { + if ( isset( $_GET['taxonomy'] ) && 'fb_product_set' === $_GET['taxonomy'] ) { + $is_product_sets_sync_enbaled = facebook_for_woocommerce()->get_rollout_switches()->is_switch_enabled( + RolloutSwitches::SWITCH_PRODUCT_SETS_SYNC_ENABLED + ); + if ( $is_product_sets_sync_enbaled ) { + $fb_catalog_id = facebook_for_woocommerce()->get_integration()->get_product_catalog_id(); + + ?> +
+

Your categories now automatically sync as product sets on Facebook

+

To make changes to automatically synced sets going forward, you should edit your categories on WooCommerce. This may take some time to update initially, but then will automatically sync every few minutes. There are no changes to any sets you previously created. + To see what’s synced, go to sets in Commerce Manager.

+
+ plugin = $plugin; - $this->screens = $this->build_menu_item_array( $is_connected ); + $this->screens = $this->build_menu_item_array(); + add_action( 'admin_init', array( $this, 'add_extra_screens' ) ); add_action( 'admin_menu', array( $this, 'add_menu_item' ) ); add_action( 'wp_loaded', array( $this, 'save' ) ); add_filter( 'parent_file', array( $this, 'set_parent_and_submenu_file' ) ); @@ -58,15 +66,15 @@ public function __construct( bool $is_connected ) { /** * Arranges the tabs. If the plugin is connected to FB, Advertise tab will be first, otherwise the Connection tab will be the first tab. * - * @param bool $is_connected is Facebook connected * @since 3.0.7 */ - private function build_menu_item_array( bool $is_connected ): array { + public function build_menu_item_array(): array { $advertise = [ Settings_Screens\Advertise::ID => new Settings_Screens\Advertise() ]; $connection = [ Settings_Screens\Connection::ID => new Settings_Screens\Connection() ]; - $first = ( $is_connected ) ? $advertise : $connection; - $last = ( $is_connected ) ? $connection : $advertise; + $is_connected = $this->plugin->get_connection_handler()->is_connected(); + $first = ( $is_connected ) ? $advertise : $connection; + $last = ( $is_connected ) ? $connection : $advertise; $screens = array( Settings_Screens\Product_Sync::ID => new Settings_Screens\Product_Sync(), @@ -76,6 +84,15 @@ private function build_menu_item_array( bool $is_connected ): array { return array_merge( array_merge( $first, $screens ), $last ); } + public function add_extra_screens(): void { + $rollout_switches = $this->plugin->get_rollout_switches(); + $is_connected = $this->plugin->get_connection_handler()->is_connected(); + $is_whatsapp_utility_messaging_enabled = $rollout_switches->is_switch_enabled( RolloutSwitches::WHATSAPP_UTILITY_MESSAGING ); + if ( true === $is_connected && true === $is_whatsapp_utility_messaging_enabled ) { + $this->screens[ Settings_Screens\Whatsapp_Utility::ID ] = new Settings_Screens\Whatsapp_Utility(); + } + } + /** * Adds the Facebook menu item. * @@ -219,7 +236,18 @@ public function render_tabs( $current_tab ) { ?> get_plugin_url() . '/assets/css/admin/facebook-for-woocommerce-advertise.css', array(), \WC_Facebookcommerce::VERSION ); + wp_enqueue_style( 'wc-facebook-admin-whatsapp-banner', facebook_for_woocommerce()->get_plugin_url() . '/assets/css/admin/facebook-for-woocommerce-whatsapp-banner.css', array(), \WC_Facebookcommerce::VERSION ); } @@ -209,7 +210,11 @@ public function render() { $fbe_extras = wp_json_encode( $this->get_lwi_ads_configuration_data() ); + $wa_banner = new \WC_Facebookcommerce_Admin_Banner(); + $wa_banner->render_banner(); + $wa_banner->enqueue_banner_script(); ?> +
initHook(); + + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); + } + + /** + * Initializes this whatsapp utility settings page's properties. + */ + public function initHook(): void { + $this->id = self::ID; + $this->label = __( 'Utility messages', 'facebook-for-woocommerce' ); + $this->title = __( 'Utility messages', 'facebook-for-woocommerce' ); + } + + /** + * Enqueue the assets. + * + * @internal + * + * @since 2.0.0 + */ + public function enqueue_assets() { + + if ( ! $this->is_current_screen_page() ) { + return; + } + + wp_enqueue_style( 'wc-facebook-admin-whatsapp-settings', facebook_for_woocommerce()->get_plugin_url() . '/assets/css/admin/facebook-for-woocommerce-whatsapp-utility.css', array(), \WC_Facebookcommerce::VERSION ); + wp_enqueue_script( + 'facebook-for-woocommerce-connect-whatsapp', + facebook_for_woocommerce()->get_asset_build_dir_url() . '/admin/whatsapp-connection.js', + array( 'jquery', 'jquery-blockui', 'jquery-tiptip', 'wc-enhanced-select' ), + \WC_Facebookcommerce::PLUGIN_VERSION + ); + $waba_id = get_option( 'wc_facebook_wa_integration_waba_id', '' ); + $whatsapp_connected = ! empty( $waba_id ); + wp_localize_script( + 'facebook-for-woocommerce-connect-whatsapp', + 'facebook_for_woocommerce_whatsapp_onboarding_progress', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'facebook-for-wc-whatsapp-onboarding-progress-nonce' ), + 'whatsapp_onboarding_complete' => $whatsapp_connected, + 'i18n' => array( + 'result' => true, + ), + ) + ); + wp_enqueue_script( + 'facebook-for-woocommerce-whatsapp-consent', + facebook_for_woocommerce()->get_asset_build_dir_url() . '/admin/whatsapp-consent.js', + array( 'jquery', 'jquery-blockui', 'jquery-tiptip', 'wc-enhanced-select' ), + \WC_Facebookcommerce::PLUGIN_VERSION + ); + $consent_collection_enabled = get_option( 'wc_facebook_whatsapp_consent_collection_setting_status', null ) === 'enabled'; + wp_localize_script( + 'facebook-for-woocommerce-whatsapp-consent', + 'facebook_for_woocommerce_whatsapp_consent', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'facebook-for-wc-whatsapp-consent-nonce' ), + 'whatsapp_onboarding_complete' => $whatsapp_connected, + 'consent_collection_enabled' => $consent_collection_enabled, + 'i18n' => array( + 'result' => true, + ), + ) + ); + $is_payment_setup = (bool) get_option( 'wc_facebook_wa_integration_is_payment_setup', null ); + wp_enqueue_script( + 'facebook-for-woocommerce-whatsapp-billing', + facebook_for_woocommerce()->get_asset_build_dir_url() . '/admin/whatsapp-billing.js', + array( 'jquery', 'jquery-blockui', 'jquery-tiptip', 'wc-enhanced-select' ), + \WC_Facebookcommerce::PLUGIN_VERSION + ); + wp_localize_script( + 'facebook-for-woocommerce-whatsapp-billing', + 'facebook_for_woocommerce_whatsapp_billing', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'facebook-for-wc-whatsapp-billing-nonce' ), + 'consent_collection_enabled' => $consent_collection_enabled, + 'is_payment_setup' => $is_payment_setup, + 'i18n' => array( + 'result' => true, + ), + ) + ); + $order_placed_event_config_id = get_option( 'wc_facebook_wa_order_placed_event_config_id', null ); + $order_placed_language = get_option( 'wc_facebook_wa_order_placed_language', 'en' ); + $order_fulfilled_event_config_id = get_option( 'wc_facebook_wa_order_fulfilled_event_config_id', null ); + $order_fulfilled_language = get_option( 'wc_facebook_wa_order_fulfilled_language', 'en' ); + $order_refunded_event_config_id = get_option( 'wc_facebook_wa_order_refunded_event_config_id', null ); + $order_refunded_language = get_option( 'wc_facebook_wa_order_refunded_language', 'en' ); + wp_enqueue_script( + 'facebook-for-woocommerce-whatsapp-events', + facebook_for_woocommerce()->get_asset_build_dir_url() . '/admin/whatsapp-events.js', + array( 'jquery', 'jquery-blockui', 'jquery-tiptip', 'wc-enhanced-select' ), + \WC_Facebookcommerce::PLUGIN_VERSION + ); + wp_localize_script( + 'facebook-for-woocommerce-whatsapp-events', + 'facebook_for_woocommerce_whatsapp_events', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'facebook-for-wc-whatsapp-events-nonce' ), + 'event' => $this->get_current_event(), + 'order_placed_enabled' => ! empty( $order_placed_event_config_id ), + 'order_placed_language' => $order_placed_language, + 'order_fulfilled_enabled' => ! empty( $order_fulfilled_event_config_id ), + 'order_fulfilled_language' => $order_fulfilled_language, + 'order_refunded_enabled' => ! empty( $order_refunded_event_config_id ), + 'order_refunded_language' => $order_refunded_language, + 'i18n' => array( + 'result' => true, + 'generic_error' => __( 'Something went wrong. Please try again.', 'facebook-for-woocommerce' ), + + ), + ) + ); + wp_enqueue_script( + 'facebook-for-woocommerce-whatsapp-finish', + facebook_for_woocommerce()->get_asset_build_dir_url() . '/admin/whatsapp-finish.js', + array( 'jquery', 'jquery-blockui', 'jquery-tiptip', 'wc-enhanced-select' ), + \WC_Facebookcommerce::PLUGIN_VERSION + ); + wp_localize_script( + 'facebook-for-woocommerce-whatsapp-finish', + 'facebook_for_woocommerce_whatsapp_finish', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'facebook-for-wc-whatsapp-finish-nonce' ), + 'i18n' => array( // will generate i18 pot translation + 'payment_setup_error' => __( 'To proceed, add a payment method to make future purchases on your accounts.', 'facebook-for-woocommerce' ), + 'onboarding_incomplete_error' => __( 'Whatsapp Business Account Onboarding is not complete or has failed.', 'facebook-for-woocommerce' ), + 'generic_error' => __( 'Something went wrong. Please try again.', 'facebook-for-woocommerce' ), + ), + ) + ); + wp_enqueue_script( + 'facebook-for-woocommerce-whatsapp-consent-remove', + facebook_for_woocommerce()->get_asset_build_dir_url() . '/admin/whatsapp-consent-remove.js', + array( 'jquery', 'jquery-blockui', 'jquery-tiptip', 'wc-enhanced-select' ), + \WC_Facebookcommerce::PLUGIN_VERSION + ); + wp_localize_script( + 'facebook-for-woocommerce-whatsapp-consent-remove', + 'facebook_for_woocommerce_whatsapp_consent_remove', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'facebook-for-wc-whatsapp-consent-disable-nonce' ), + 'i18n' => array( + 'result' => true, + ), + ) + ); + wp_enqueue_script( + 'facebook-for-woocommerce-whatsapp-templates', + facebook_for_woocommerce()->get_asset_build_dir_url() . '/admin/whatsapp-templates.js', + array( 'jquery', 'jquery-blockui', 'jquery-tiptip', 'wc-enhanced-select' ), + \WC_Facebookcommerce::PLUGIN_VERSION + ); + wp_localize_script( + 'facebook-for-woocommerce-whatsapp-templates', + 'facebook_for_woocommerce_whatsapp_templates', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'facebook-for-wc-whatsapp-templates-nonce' ), + 'i18n' => array( + 'result' => true, + ), + ) + ); + wp_enqueue_script( + 'facebook-for-woocommerce-whatsapp-disconnect', + facebook_for_woocommerce()->get_asset_build_dir_url() . '/admin/whatsapp-disconnect.js', + array( 'jquery', 'jquery-blockui', 'jquery-tiptip', 'wc-enhanced-select' ), + \WC_Facebookcommerce::PLUGIN_VERSION + ); + wp_localize_script( + 'facebook-for-woocommerce-whatsapp-disconnect', + 'facebook_for_woocommerce_whatsapp_disconnect', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'facebook-for-wc-whatsapp-disconnect-nonce' ), + 'i18n' => array( + 'result' => true, + ), + ) + ); + } + + + /** + * Renders the screen. + * + * @since 2.0.0 + */ + public function render() { + $view = $this->get_current_view(); + if ( 'utility_settings' === $view ) { + $this->render_utility_message_overview(); + } elseif ( in_array( $view, self::MANAGE_EVENT_VIEWS, true ) ) { + $this->render_manage_events_view(); + } else { + $this->render_utility_message_onboarding(); + } + parent::render(); + } + + /** + * Renders the WhatsApp Utility Onboarding screen. + */ + public function render_utility_message_onboarding() { + + ?> + +
+
+
+

+ +
+
+
+
+
+
+
+
+

+

+
+
+
+ +
+
+
+
+
+ + + +
+

+ +
+
+ +
+
+
+
+
+
+
+
+

+
+

+ + +

+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+

+

+ +

+
+
+
+
+
+
+

+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+

+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+

+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+

+ +
+ + + + + +
+ + +
+
+
+

+
+ +
+ +
+
+
+
+
+
+
+

+
+ +
+
+
+ +
+
+

+
+ +
+ +
+
+
+ get_current_event(); + ?> +
+
+
+

+ + + + + + + +

+

+ + + + + + + +

+
+
+
+
+

+ +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ +
+ log( 'Initial full product sync disabled by filter hook `facebook_for_woocommerce_allow_full_batch_api_sync`', 'facebook_for_woocommerce_connect' ); } + facebook_for_woocommerce()->get_product_sets_sync_handler()->sync_all_product_sets(); update_option( 'wc_facebook_has_connected_fbe_2', 'yes' ); update_option( 'wc_facebook_has_authorized_pages_read_engagement', 'yes' ); // redirect to the Commerce onboarding if directed to do so diff --git a/includes/Handlers/WhatsAppUtilityConnection.php b/includes/Handlers/WhatsAppUtilityConnection.php new file mode 100644 index 000000000..2a8334f18 --- /dev/null +++ b/includes/Handlers/WhatsAppUtilityConnection.php @@ -0,0 +1,529 @@ + 'order_management_4', + 'ORDER_FULFILLED' => 'shipment_confirmation_4', + 'ORDER_REFUNDED' => 'refund_confirmation_1', + ); + + /** @var string Default language for Library Template */ + const DEFAULT_LANGUAGE = 'en'; + + /** + * Makes an API call to Template Library API + * + * @param string $event Order Management Event + * @param string $bisu_token the BISU token received in the webhook + */ + public static function get_template_library_content( $event, $bisu_token ) { + wc_get_logger()->info( + sprintf( + __( 'In Template Library Get API call ', 'facebook-for-woocommerce' ), + ) + ); + $base_url = array( self::GRAPH_API_BASE_URL, self::API_VERSION, 'message_template_library' ); + $base_url = esc_url( implode( '/', $base_url ) ); + $library_name = self::EVENT_TO_LIBRARY_TEMPLATE_MAPPING[ $event ]; + + $params = array( + 'name' => $library_name, + 'language' => self::DEFAULT_LANGUAGE, + 'access_token' => $bisu_token, + ); + $url = add_query_arg( $params, $base_url ); + $options = array( + 'headers' => array( + 'Authorization' => $bisu_token, + ), + 'body' => array(), + 'timeout' => 300, // 5 minutes + ); + + $response = wp_remote_request( $url, $options ); + $status_code = wp_remote_retrieve_response_code( $response ); + $data = wp_remote_retrieve_body( $response ); + if ( is_wp_error( $response ) || 200 !== $status_code ) { + wc_get_logger()->info( + sprintf( + /* translators: %s $error_message */ + __( 'Template Library GET API call Failed %1$s ', 'facebook-for-woocommerce' ), + $data, + ) + ); + wp_send_json_error( $response, 'Template Library GET API call Failed' ); + } else { + wc_get_logger()->info( + sprintf( + __( 'Template Library GET API call Succeeded', 'facebook-for-woocommerce' ) + ) + ); + wp_send_json_success( $data, 'Finish Template Library API Call' ); + } + } + + /** + * Makes an API call to Whatsapp Utility Message Connect API + * + * @param string $waba_id WABA ID + * @param string $wacs_id WACS ID + * @param string $external_business_id external business ID + * @param string $bisu_token BISU token + */ + public static function wc_facebook_whatsapp_connect_utility_messages_call( $waba_id, $wacs_id, $external_business_id, $bisu_token ) { + $base_url = array( self::GRAPH_API_BASE_URL, self::API_VERSION, $waba_id, 'connect_utility_messages' ); + $base_url = esc_url( implode( '/', $base_url ) ); + $query_params = array( + 'external_integration_id' => $external_business_id, + 'wacs_id' => $wacs_id, + 'access_token' => $bisu_token, + ); + $base_url = add_query_arg( $query_params, $base_url ); + $options = array( + 'headers' => array( + 'Authorization' => $bisu_token, + ), + 'body' => array(), + 'timeout' => 300, // 5 minutes + ); + $response = wp_remote_post( $base_url, $options ); + wc_get_logger()->info( + sprintf( + /* translators: %s $response */ + __( 'Connect Whatsapp Utility Message API Response: %1$s ', 'facebook-for-woocommerce' ), + json_encode( $response ), + ) + ); + $response_body = explode( "\n", wp_remote_retrieve_body( $response ) ); + $response_data = json_decode( $response_body[0] ); + if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { + $error_message = $response_data->error->error_user_title ?? $response_data->error->message ?? 'Something went wrong. Please try again later!'; + + wc_get_logger()->info( + sprintf( + /* translators: %s $error_message */ + __( 'Connect Whatsapp Utility Message API Call Failure %1$s ', 'facebook-for-woocommerce' ), + $error_message, + ) + ); + wp_send_json_error( $error_message, 'Finish Onboarding Failure' ); + } else { + $integration_config_id = $response_data->id; + wc_get_logger()->info( + sprintf( + /* translators: %s $integration_config_id */ + __( 'Connect Whatsapp Utility Message API Call Success!!! Integration ID: %1$s!!!', 'facebook-for-woocommerce' ), + $integration_config_id, + ) + ); + update_option( 'wc_facebook_wa_integration_config_id', $integration_config_id ); + wp_send_json_success( $response, 'Finish Onboarding Success' ); + } + } + + /** + * Makes an API call to Whatsapp Utility Message Disconnect API and delete the options in DB + * + * @param string $waba_id WABA ID + * @param string $integration_config_id whatsapp integration config ID + * @param string $bisu_token BISU token + */ + public static function wc_facebook_disconnect_whatsapp( $waba_id, $integration_config_id, $bisu_token ) { + $base_url = array( self::GRAPH_API_BASE_URL, self::API_VERSION, $waba_id, 'disconnect_utility_messages' ); + $base_url = esc_url( implode( '/', $base_url ) ); + $query_params = array( + 'integration_config_id' => $integration_config_id, + 'access_token' => $bisu_token, + ); + $base_url = add_query_arg( $query_params, $base_url ); + $options = array( + 'headers' => array( + 'Authorization' => $bisu_token, + ), + 'body' => array(), + 'timeout' => 300, // 5 minutes + ); + $response = wp_remote_post( $base_url, $options ); + wc_get_logger()->info( + sprintf( + /* translators: %s $error_message */ + __( 'Disconnect Whatsapp Utility Message API Call Response: %1$s ', 'facebook-for-woocommerce' ), + json_encode( $response ), + ) + ); + if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { + $error_data = explode( "\n", wp_remote_retrieve_body( $response ) ); + $error_object = json_decode( $error_data[0] ); + $error_message = $error_object->error->error_user_title ?? $error_object->error->message ?? 'Something went wrong. Please try again later!'; + + wc_get_logger()->info( + sprintf( + /* translators: %s $error_message */ + __( 'Disconnect Whatsapp Utility Message API Call Error: %1$s ', 'facebook-for-woocommerce' ), + $error_message, + ) + ); + wp_send_json_error( $error_message, 'Disconnect Whatsapp Failure' ); + } else { + wc_get_logger()->info( + sprintf( + __( 'Disconnect Whatsapp Utility Message API Call Success!!!', 'facebook-for-woocommerce' ) + ) + ); + + // delete all the whatsapp setting options in DB + $wa_settings = array( + 'wc_facebook_wa_integration_waba_id', + 'wc_facebook_wa_integration_bisu_access_token', + 'wc_facebook_wa_integration_business_id', + 'wc_facebook_wa_integration_wacs_phone_number', + 'wc_facebook_wa_integration_is_payment_setup', + 'wc_facebook_wa_integration_wacs_id', + 'wc_facebook_wa_integration_waba_profile_picture_url', + 'wc_facebook_wa_integration_waba_display_name', + 'wc_facebook_whatsapp_consent_collection_setting_status', + 'wc_facebook_wa_integration_config_id', + 'wc_facebook_wa_order_placed_event_config_id', + 'wc_facebook_wa_order_placed_language', + 'wc_facebook_wa_order_fulfilled_event_config_id', + 'wc_facebook_wa_order_fulfilled_language', + 'wc_facebook_wa_order_refunded_event_config_id', + 'wc_facebook_wa_order_refunded_language', + ); + + self::wc_facebook_whatsapp_settings_delete( $wa_settings ); + + wc_get_logger()->info( + sprintf( + __( 'Disconnect Whatsapp Utility Message - Whatsapp Settings Deletion Success!!!', 'facebook-for-woocommerce' ) + ) + ); + + wp_send_json_success( $response, 'Disconnect Whatsapp Success' ); + } + } + + public static function wc_facebook_whatsapp_settings_delete( $wa_settings ) { + foreach ( $wa_settings as $setting ) { + delete_option( $setting ); // this only deletes if option exists, no error on failure + } + } + + /** + * Makes an API call to Whatsapp Utility Event Configs Post API to create or update Event Configs + * + * @param string $event Order Management Event + * @param string $integration_config_id Integration Config Id + * @param string $language Language Code + * @param string $status ACTIVE or INACTIVE + * @param string $bisu_token the BISU token received in the webhook + */ + public static function post_whatsapp_utility_messages_event_configs_call( $event, $integration_config_id, $language, $status, $bisu_token ) { + $base_url = array( self::GRAPH_API_BASE_URL, self::API_VERSION, $integration_config_id, 'event_configs' ); + $base_url = esc_url( implode( '/', $base_url ) ); + $account_url = get_permalink( get_option( 'woocommerce_myaccount_page_id' ) ); + $view_orders_endpoint = get_option( 'woocommerce_myaccount_view_order_endpoint' ); + $view_orders_base_url = esc_url( $account_url . $view_orders_endpoint ); + // Order Refunded template has no CTA + $library_template_button_inputs = 'ORDER_REFUNDED' === $event ? array() : array( + array( + 'type' => 'URL', + 'url' => array( + // View Url is dynamic and has order_id as suffix + 'base_url' => "$view_orders_base_url/{{1}}", + // Example view orders url with order id: 1234 + 'url_suffix_example' => "$view_orders_base_url/1234", + ), + ), + ); + $query_params = array( + 'event' => $event, + 'language' => $language, + 'status' => $status, + 'library_template_name' => self::EVENT_TO_LIBRARY_TEMPLATE_MAPPING[ $event ], + 'library_template_button_inputs' => $library_template_button_inputs, + 'access_token' => $bisu_token, + ); + $base_url = add_query_arg( $query_params, $base_url ); + $options = array( + 'headers' => array( + 'Authorization' => $bisu_token, + ), + 'body' => array(), + 'timeout' => 300, // 5 minutes + ); + $response = wp_remote_post( $base_url, $options ); + $status_code = wp_remote_retrieve_response_code( $response ); + $data = explode( "\n", wp_remote_retrieve_body( $response ) ); + $response_object = json_decode( $data[0] ); + $is_error = is_wp_error( $response ); + wc_get_logger()->info( + sprintf( + /* translators: %s $error_message */ + __( 'Event Configs Post API call Response: %1$s ', 'facebook-for-woocommerce' ), + json_encode( $response ), + ) + ); + if ( is_wp_error( $response ) || 200 !== $status_code ) { + $error_message = $response_object->error->error_user_title ?? $response_object->error->message ?? 'Something went wrong. Please try again later!'; + wc_get_logger()->info( + sprintf( + /* translators: %s $error_message %s status code %s is_wp_error value*/ + __( 'Event Configs Post API call Failed with Error: %1$s, Status code: %2$d, Is Wp Error: %3$s', 'facebook-for-woocommerce' ), + $error_message, + $status_code, + (string) $is_error, + ) + ); + wp_send_json_error( $response, 'Event Configs Post API call Failed' ); + } else { + $event_config_id_option_name = implode( '_', array( self::WA_UTILITY_OPTION_PREFIX, strtolower( $event ), 'event_config_id' ) ); + $event_config_language_option_name = implode( '_', array( self::WA_UTILITY_OPTION_PREFIX, strtolower( $event ), 'language' ) ); + $event_config_id = $response_object->id; + $event_status = $response_object->status; + $language = $response_object->language; + wc_get_logger()->info( + sprintf( + /* translators: %s $option_name %s $event_config_id %s $event_status */ + __( 'Event Configs Post API call Succeeded. API Response Event Config id: %1$s, Event Status: %2$s, Language: %3$s', 'facebook-for-woocommerce' ), + $event_config_id, + $event_status, + $language, + ) + ); + if ( 'ACTIVE' === $event_status ) { + update_option( $event_config_id_option_name, $event_config_id ); + update_option( $event_config_language_option_name, $language ); + } else { + $settings = array( + $event_config_id_option_name, + $event_config_language_option_name, + ); + self::wc_facebook_whatsapp_settings_delete( + $settings + ); + } + wp_send_json_success( 'Event Configs Post API call Completed' ); + } + } + + + /** + * Makes an API call to Event Processor: Message Events Post API to send whatsapp utility messages + * + * @param string $event Order Managerment event + * @param string $event_config_id Event Config Id + * @param string $language_code Language code + * @param string $wacs_id Whatsapp Phone Number id + * @param string $order_id Order id + * @param string $phone_number Customer phone number + * @param string $first_name Customer first name + * @param int $refund_value Amount refunded to the Customer + * @param string $currency Currency code + * @param string $bisu_token the BISU token received in the webhook + */ + public static function post_whatsapp_utility_messages_events_call( $event, $event_config_id, $language_code, $wacs_id, $order_id, $phone_number, $first_name, $refund_value, $currency, $bisu_token ) { + $base_url = array( self::GRAPH_API_BASE_URL, self::API_VERSION, $wacs_id, "message_events?access_token=$bisu_token" ); + $base_url = esc_url( implode( '/', $base_url ) ); + $name = self::EVENT_TO_LIBRARY_TEMPLATE_MAPPING[ $event ]; + $components = self::get_components_for_event( $event, $order_id, $first_name, $refund_value, $currency ); + $options = array( + 'body' => array( + 'messaging_product' => 'whatsapp', + 'to' => $phone_number, + 'event_config_id' => $event_config_id, + 'external_event_id' => "${order_id}", + 'template' => array( + 'language' => array( + 'code' => $language_code, + ), + 'components' => $components, + ), + 'type' => 'template', + ), + 'timeout' => 300, // 5 minutes + ); + $response = wp_remote_post( $base_url, $options ); + $status_code = wp_remote_retrieve_response_code( $response ); + $data = explode( "\n", wp_remote_retrieve_body( $response ) ); + $response_object = json_decode( $data[0] ); + wc_get_logger()->info( + sprintf( + /* translators: %s $error_message */ + __( 'Message Events Post API call Response: %1$s ', 'facebook-for-woocommerce' ), + json_encode( $response ), + ) + ); + if ( is_wp_error( $response ) || 200 !== $status_code ) { + $error_message = $response_object->error->error_user_title ?? $response_object->error->message ?? 'Something went wrong. Please try again later!'; + wc_get_logger()->info( + sprintf( + /* translators: %s $order_id %s $error_message */ + __( 'Message Events Post API call for Order id %1$s Failed %2$s ', 'facebook-for-woocommerce' ), + $order_id, + $error_message, + ) + ); + } else { + wc_get_logger()->info( + sprintf( + /* translators: %s $order_id */ + __( 'Message Events Post API call for Order id %1$s Succeeded.', 'facebook-for-woocommerce' ), + $order_id + ) + ); + } + } + + /** + * Makes an API call to Integration Config Get API + * + * @param string $integration_config_id Integration Config id + * @param string $bisu_token the BISU token received in the webhook + */ + public static function get_supported_languages_for_templates( $integration_config_id, $bisu_token ) { + $base_url = array( self::GRAPH_API_BASE_URL, self::API_VERSION, $integration_config_id ); + $base_url = esc_url( implode( '/', $base_url ) ); + $params = array( + 'access_token' => $bisu_token, + ); + $url = add_query_arg( $params, $base_url ); + $options = array( + 'headers' => array( + 'Authorization' => $bisu_token, + ), + 'body' => array(), + 'timeout' => 300, // 5 minutes + ); + + $response = wp_remote_request( $url, $options ); + $status_code = wp_remote_retrieve_response_code( $response ); + $data = wp_remote_retrieve_body( $response ); + if ( is_wp_error( $response ) || 200 !== $status_code ) { + wc_get_logger()->info( + sprintf( + /* translators: %s $error_message */ + __( 'Integration Config GET API call Failed %1$s ', 'facebook-for-woocommerce' ), + $data, + ) + ); + wp_send_json_error( $response, 'Integration Config GET API call Failed' ); + } else { + wc_get_logger()->info( + sprintf( + __( 'Integration Config GET API call Succeeded', 'facebook-for-woocommerce' ) + ) + ); + // $response_object = json_decode( $data[0] ); + wp_send_json_success( $data, 'Finish Integration Config API Call' ); + } + } + + + /** + * Gets Component Objects for Order Management Events + * + * @param string $event Order Management event + * @param string $order_id Order id + * @param string $first_name Customer first name + * @param string $refund_value Amount refunded to the Customer + * @param string $currency Currency code + */ + public static function get_components_for_event( $event, $order_id, $first_name, $refund_value, $currency ) { + if ( 'ORDER_REFUNDED' === $event ) { + return array( + array( + 'type' => 'HEADER', + 'parameters' => array( + array( + 'type' => 'currency', + 'currency' => array( + 'fallback_value' => 'VALUE', + 'code' => $currency, + 'amount_1000' => $refund_value, + ), + ), + ), + ), + array( + 'type' => 'BODY', + 'parameters' => array( + array( + 'type' => 'text', + 'text' => $first_name, + ), + array( + 'type' => 'currency', + 'currency' => array( + 'fallback_value' => 'VALUE', + 'code' => $currency, + 'amount_1000' => $refund_value, + ), + ), + array( + 'type' => 'text', + 'text' => "#$order_id", + ), + ), + ), + ); + } else { + return array( + array( + 'type' => 'BODY', + 'parameters' => array( + array( + 'type' => 'text', + 'text' => $first_name, + ), + array( + 'type' => 'text', + 'text' => "#$order_id", + ), + ), + ), + array( + 'type' => 'BUTTON', + 'sub_type' => 'url', + 'index' => 0, + 'parameters' => array( + array( + 'type' => 'text', + 'text' => "$order_id", + ), + ), + ), + ); + } + } +} diff --git a/includes/Handlers/Whatsapp_Webhook.php b/includes/Handlers/Whatsapp_Webhook.php new file mode 100644 index 000000000..4d613265e --- /dev/null +++ b/includes/Handlers/Whatsapp_Webhook.php @@ -0,0 +1,209 @@ + array( 'POST' ), + 'callback' => array( $this, 'whatsapp_webhook_callback' ), + 'permission_callback' => '__return_true', + ), + ) + ); + } + + /** + * Updates Facebook settings options. + * + * @param array $settings Array of settings to update. + * + * @return bool + * @internal + */ + private static function update_settings( $settings ) { + $updated = array(); + foreach ( $settings as $key => $value ) { + if ( ! empty( $key ) ) { + $updated[ $key ] = update_option( $key, $value ); + } + } + // if any of setting updates failed, return false + return ! in_array( false, $updated, true ); + } + + /** + * Authenticates Whatsapp Webhook using the SHA1 of the business ID and BISU token + * + * @param string $auth_key the auth key received in the webhook + * @param string $bisu_token the BISU token received in the webhook + * + * @return bool + * @internal + */ + private static function authenticate_request( $auth_key, $bisu_token ) { + $business_id = get_option( 'wc_facebook_business_manager_id' ); + + $expected_auth_key = 'sha1=' . (string) hash_hmac( 'sha1', $bisu_token, $business_id ); + + return hash_equals( $expected_auth_key, $auth_key ); + } + + + + /** + * Whatsapp Webhook Listener + * + * @since 2.3.0 + * @see Connection + * + * @param \WP_REST_Request $request The request. + * @return \WP_REST_Response + */ + public function whatsapp_webhook_callback( \WP_REST_Request $request ) { + try { + $request_params = $request->get_params(); + $waba_id = sanitize_text_field( $request_params['wabaId'] ); + $wacs_id = sanitize_text_field( $request_params['wacsId'] ); + $is_waba_payment_setup = sanitize_text_field( $request_params['isWabaPaymentSetup'] ); + $waba_profile_picture_url = sanitize_text_field( $request_params['wabaProfilePictureUrl'] ); + $bisu_token = sanitize_text_field( $request_params['clientBisuToken'] ); + $business_id = sanitize_text_field( $request_params['clientBusinessId'] ); + $wacs_phone_number = sanitize_text_field( $request_params['wacsPhoneNumber'] ); + $waba_display_name = sanitize_text_field( $request_params['wabaDisplayName'] ); + $auth_key = sanitize_text_field( $request_params['authKey'] ); + + // authentication is done via auth_key using sha_1 hash mac of BISU token and business ID stored in woo DB + $authentication_result = self::authenticate_request( $auth_key, $bisu_token ); + + if ( false === $authentication_result ) { + wc_get_logger()->info( + sprintf( + __( 'Authentication Failure on received Whatsapp Webhook', 'facebook-for-woocommerce' ), + ) + ); + return new \WP_REST_Response( + [ + 'success' => false, + 'message' => 'Authentication Failure on received Whatsapp Webhook', + ], + 400 + ); + } + + if ( empty( $waba_id ) || empty( $bisu_token ) || empty( $business_id ) || empty( $wacs_phone_number ) || empty( $wacs_id ) ) { + wc_get_logger()->info( + sprintf( + __( 'All required onboarding info not received in Whatsapp Webhook', 'facebook-for-woocommerce' ), + ) + ); + return new \WP_REST_Response( + [ + 'success' => false, + 'message' => 'All required onboarding info not received in Whatsapp Webhook', + ], + 400 + ); + } + + wc_get_logger()->info( + sprintf( + /* translators: %s waba ID %s business ID */ + __( 'Whatsapp Account WebHook Event received. WABA ID: %1$s, Business ID: %2$s ', 'facebook-for-woocommerce' ), + $waba_id, + $business_id + ) + ); + + $options_setting_fields = array( + 'wc_facebook_wa_integration_waba_id' => $waba_id, + 'wc_facebook_wa_integration_bisu_access_token' => $bisu_token, + 'wc_facebook_wa_integration_business_id' => $business_id, + 'wc_facebook_wa_integration_wacs_phone_number' => $wacs_phone_number, + 'wc_facebook_wa_integration_is_payment_setup' => $is_waba_payment_setup, + 'wc_facebook_wa_integration_wacs_id' => $wacs_id, + 'wc_facebook_wa_integration_waba_profile_picture_url' => $waba_profile_picture_url, + 'wc_facebook_wa_integration_waba_display_name' => $waba_display_name, + + ); + + $result = self::update_settings( $options_setting_fields ); + + if ( false === $result ) { + wc_get_logger()->info( + sprintf( + /* translators: %d $waba_id, %d $business_id. */ + __( 'Whatsapp Integration Setting Fields Update Failure waba_id: %1$s, business_id: %2$s', 'facebook-for-woocommerce' ), + $waba_id, + $business_id, + ) + ); + + return new \WP_REST_Response( + [ + 'success' => false, + 'message' => 'Whatsapp Integration Setting Fields Update Failure', + ], + 400 + ); + + } + + wc_get_logger()->info( + sprintf( + /* translators: %d $waba_id, %d $business_id. */ + __( 'Whatsapp Integration Setting Fields stored successfully in wp_options. wc_facebook_wa_integration_waba_id: %1$s, wc_facebook_wa_integration_business_id: %2$s ', 'facebook-for-woocommerce' ), + $waba_id, + $business_id, + ) + ); + + return new \WP_REST_Response( [ 'success' => true ], 200 ); + } catch ( \Exception $e ) { + return $this->error_response( + [ + 'success' => false, + 'message' => $e->getMessage(), + ], + 500 + ); + } + } +} diff --git a/includes/Lifecycle.php b/includes/Lifecycle.php index ed490e14d..4c51d3523 100644 --- a/includes/Lifecycle.php +++ b/includes/Lifecycle.php @@ -53,7 +53,8 @@ public function __construct( Framework\Plugin $plugin ) { '2.0.4', '2.4.0', '2.5.0', - '3.2.0' + '3.2.0', + '3.4.9' ); } @@ -336,4 +337,13 @@ protected function upgrade_to_3_2_0() { delete_option( self::SETTING_MESSENGER_COLOR_HEX ); } + /** + * Trigger sync of all WooCommerce categories + * + * @since 3.4.9 + */ + protected function upgrade_to_3_4_9() { + facebook_for_woocommerce()->get_product_sets_sync_handler()->sync_all_product_sets(); + } + } diff --git a/includes/ProductSets/ProductSetSync.php b/includes/ProductSets/ProductSetSync.php new file mode 100644 index 000000000..091ca6fc0 --- /dev/null +++ b/includes/ProductSets/ProductSetSync.php @@ -0,0 +1,246 @@ +add_hooks(); + } + + + /** + * Adds needed hooks to support product set sync. + */ + private function add_hooks() { + /** + * Sets up hooks to synchronize WooCommerce category mutations (create, update, delete) with Meta catalog's product sets in real-time. + */ + add_action( 'create_' . self::WC_PRODUCT_CATEGORY_TAXONOMY, array( $this, 'on_create_or_update_product_wc_category_callback' ), 99, 3 ); + add_action( 'edited_' . self::WC_PRODUCT_CATEGORY_TAXONOMY, array( $this, 'on_create_or_update_product_wc_category_callback' ), 99, 3 ); + add_action( 'delete_' . self::WC_PRODUCT_CATEGORY_TAXONOMY, array( $this, 'on_delete_wc_product_category_callback' ), 99, 4 ); + + /** + * Schedules a daily sync of all WooCommerce categories to ensure any missed real-time updates are captured. + */ + add_action( Heartbeat::DAILY, array( $this, 'sync_all_product_sets' ) ); + } + + /** + * @since 3.4.9 + * + * @param int $term_id Term ID. + * @param int $tt_id Term taxonomy ID. + * @param array $args Arguments. + */ + public function on_create_or_update_product_wc_category_callback( $term_id, $tt_id, $args ) { + try { + if ( ! $this->is_sync_enabled() ) { + return; + } + + $wc_category = get_term( $term_id, self::WC_PRODUCT_CATEGORY_TAXONOMY ); + $fb_product_set_id = $this->get_fb_product_set_id( $wc_category ); + if ( ! empty( $fb_product_set_id ) ) { + $this->update_fb_product_set( $wc_category, $fb_product_set_id ); + } else { + $this->create_fb_product_set( $wc_category ); + } + } catch ( \Exception $exception ) { + $this->log_exception( $exception ); + } + } + + /** + * @since 3.4.9 + * + * @param int $term_id Term ID. + * @param int $tt_id Term taxonomy ID. + * @param WP_Term $deleted_term Copy of the already-deleted term. + * @param array $object_ids List of term object IDs. + */ + public function on_delete_wc_product_category_callback( $term_id, $tt_id, $deleted_term, $object_ids ) { + try { + if ( ! $this->is_sync_enabled() ) { + return; + } + + $fb_product_set_id = $this->get_fb_product_set_id( $deleted_term ); + if ( ! empty( $fb_product_set_id ) ) { + $this->delete_fb_product_set( $fb_product_set_id ); + } + } catch ( \Exception $exception ) { + $this->log_exception( $exception ); + } + } + + /** + * @since 3.4.9 + */ + public function sync_all_product_sets() { + try { + if ( ! $this->is_sync_enabled() ) { + return; + } + + $this->sync_all_wc_product_categories(); + } catch ( \Exception $exception ) { + $this->log_exception( $exception ); + } + } + + protected function is_sync_enabled() { + return facebook_for_woocommerce()->get_rollout_switches()->is_switch_enabled( + RolloutSwitches::SWITCH_PRODUCT_SETS_SYNC_ENABLED + ); + } + + private function log_exception( \Exception $exception ) { + facebook_for_woocommerce()->log( + 'ProductSetSync exception' . + ': exception_code : ' . $exception->getCode() . + '; exception_class : ' . get_class( $exception ) . + ': exception_message : ' . $exception->getMessage() . + '; exception_trace : ' . $exception->getTraceAsString(), + null, + \WC_Log_Levels::ERROR + ); + } + + /** + * Important. This is ID from the WC category to be used as a retailer ID for the FB product set + * + * @param WP_Term $wc_category The WooCommerce category object. + */ + private function get_retailer_id( $wc_category ) { + return $wc_category->term_taxonomy_id; + } + + protected function get_fb_product_set_id( $wc_category ) { + $retailer_id = $this->get_retailer_id( $wc_category ); + $fb_catalog_id = facebook_for_woocommerce()->get_integration()->get_product_catalog_id(); + + try { + $response = facebook_for_woocommerce()->get_api()->read_product_set_item( $fb_catalog_id, $retailer_id ); + } catch ( \Exception $e ) { + $message = sprintf( 'There was an error trying to get product set data in a catalog: %s', $e->getMessage() ); + facebook_for_woocommerce()->log( $message ); + + /** + * Re-throw the exception to prevent potential issues, such as creating duplicate sets. + */ + throw $e; + } + + return $response->get_product_set_id(); + } + + protected function build_fb_product_set_data( $wc_category ) { + $wc_category_name = get_term_field( 'name', $wc_category, self::WC_PRODUCT_CATEGORY_TAXONOMY ); + $wc_category_description = get_term_field( 'description', $wc_category, self::WC_PRODUCT_CATEGORY_TAXONOMY ); + $wc_category_url = get_term_link( $wc_category, self::WC_PRODUCT_CATEGORY_TAXONOMY ); + $wc_category_thumbnail_id = get_term_meta( $wc_category, 'thumbnail_id', true ); + $wc_category_thumbnail_url = wp_get_attachment_image_src( $wc_category_thumbnail_id ); + + $fb_product_set_metadata = array(); + if ( ! empty( $wc_category_thumbnail_url ) ) { + $fb_product_set_metadata['cover_image_url'] = $wc_category_thumbnail_url; + } + if ( ! empty( $wc_category_description ) ) { + $fb_product_set_metadata['description'] = $wc_category_description; + } + if ( ! empty( $wc_category_url ) ) { + $fb_product_set_metadata['external_url'] = $wc_category_url; + } + + $fb_product_set_data = array( + 'name' => $wc_category_name, + 'filter' => wp_json_encode( array( 'and' => array( array( 'product_type' => array( 'i_contains' => $wc_category_name ) ) ) ) ), + 'retailer_id' => $this->get_retailer_id( $wc_category ), + 'metadata' => wp_json_encode( $fb_product_set_metadata ), + ); + + return $fb_product_set_data; + } + + protected function create_fb_product_set( $wc_category ) { + $fb_product_set_data = $this->build_fb_product_set_data( $wc_category ); + $fb_catalog_id = facebook_for_woocommerce()->get_integration()->get_product_catalog_id(); + + try { + facebook_for_woocommerce()->get_api()->create_product_set_item( $fb_catalog_id, $fb_product_set_data ); + } catch ( \Exception $e ) { + $message = sprintf( 'There was an error trying to create product set: %s', $e->getMessage() ); + facebook_for_woocommerce()->log( $message ); + } + } + + protected function update_fb_product_set( $wc_category, $fb_product_set_id ) { + $fb_product_set_data = $this->build_fb_product_set_data( $wc_category ); + + try { + facebook_for_woocommerce()->get_api()->update_product_set_item( $fb_product_set_id, $fb_product_set_data ); + } catch ( \Exception $e ) { + $message = sprintf( 'There was an error trying to update product set: %s', $e->getMessage() ); + facebook_for_woocommerce()->log( $message ); + } + } + + protected function delete_fb_product_set( $fb_product_set_id ) { + try { + $allow_live_deletion = true; + facebook_for_woocommerce()->get_api()->delete_product_set_item( $fb_product_set_id, $allow_live_deletion ); + } catch ( \Exception $e ) { + $message = sprintf( 'There was an error trying to delete product set in a catalog: %s', $e->getMessage() ); + facebook_for_woocommerce()->log( $message ); + } + } + + private function sync_all_wc_product_categories() { + $wc_product_categories = get_terms( + array( + 'taxonomy' => self::WC_PRODUCT_CATEGORY_TAXONOMY, + 'hide_empty' => false, + 'orderby' => 'ID', + 'order' => 'ASC', + ) + ); + + foreach ( $wc_product_categories as $wc_category ) { + try { + $fb_product_set_id = $this->get_fb_product_set_id( $wc_category ); + if ( ! empty( $fb_product_set_id ) ) { + $this->update_fb_product_set( $wc_category, $fb_product_set_id ); + } else { + $this->create_fb_product_set( $wc_category ); + } + } catch ( \Exception $exception ) { + $this->log_exception( $exception ); + } + } + } +} diff --git a/includes/RolloutSwitches.php b/includes/RolloutSwitches.php new file mode 100644 index 000000000..dfb77f411 --- /dev/null +++ b/includes/RolloutSwitches.php @@ -0,0 +1,101 @@ +plugin = $plugin; + add_action( Heartbeat::HOURLY, array( $this, 'init' ) ); + } + + public function init() { + $is_connected = $this->plugin->get_connection_handler()->is_connected(); + if ( ! $is_connected ) { + return; + } + + try { + $external_business_id = $this->plugin->get_connection_handler()->get_external_business_id(); + $switches = $this->plugin->get_api()->get_rollout_switches( $external_business_id ); + $data = $switches->get_data(); + foreach ( $data as $switch ) { + if ( ! isset( $switch['switch'] ) || ! $this->is_switch_active( $switch['switch'] ) ) { + continue; + } + $this->rollout_switches[ $switch['switch'] ] = (bool) $switch['enabled']; + } + } catch ( Exception $e ) { + \WC_Facebookcommerce_Utils::log_exception_immediately_to_meta( + $e, + [ + 'event' => 'rollout_switches', + 'event_type' => 'init', + ] + ); + } + } + + /** + * Get if the switch is enabled or not. + * If the switch is not active -> + * FALSE + * + * If the switch is active but not in the response -> + * TRUE: we assume this is an old version of the plugin + * and the backend since has changed and the switch was released + * in the backend we will otherwise always return false for unreleased + * features + * + * If the feature is active and in the response -> + * we will return the value of the switch from the response + * + * @param string $switch_name The name of the switch. + */ + public function is_switch_enabled( string $switch_name ) { + if ( ! $this->is_switch_active( $switch_name ) ) { + return false; + } + + return isset( $this->rollout_switches[ $switch_name ] ) ? $this->rollout_switches[ $switch_name ] : true; + } + + public function is_switch_active( string $switch_name ): bool { + return in_array( $switch_name, self::ACTIVE_SWITCHES, true ); + } +} diff --git a/package.json b/package.json index 0357d1ff5..55ee5a7b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "facebook-for-woocommerce", - "version": "3.4.8", + "version": "3.4.9", "author": "Facebook", "homepage": "https://woocommerce.com/products/facebook/", "license": "GPL-2.0", diff --git a/readme.txt b/readme.txt index 0d89311cd..50b3c7433 100644 --- a/readme.txt +++ b/readme.txt @@ -2,8 +2,8 @@ Contributors: facebook Tags: meta, facebook, conversions api, catalog sync, ads Requires at least: 5.6 -Tested up to: 6.7 -Stable tag: 3.4.8 +Tested up to: 6.8.1 +Stable tag: 3.4.9 Requires PHP: 7.4 MySQL: 5.6 or greater License: GPLv2 or later @@ -40,12 +40,26 @@ When opening a bug on GitHub, please give us as many details as possible. == Changelog == -= 3.4.8 - 2025-05-06 = -* Add - Feature to sync global attributes to Meta and test API format by @devbodaghe in #3050 -* Fix - Facebook attribute dropdown display and syncing issues by @devbodaghe in #3051 -* Tweak - Set helper text for dropdown sync by @devbodaghe in #3104 -* Tweak - Remove unused condition field code for variable products by @devbodaghe in #3114 -* Fix - Cursor style not resetting after attribute removal by @devbodaghe in #3113 -* Fix - 'Call to a member function is_taxonomy() on string' error when processing variable products by @devbodaghe in #3155 += 3.4.9 - 2025-05-14 = +* Add - Support for rollout switches in the plugin to control feature rollouts from meta side @francorisso in #3126 +* Fix - Tests in the rollout switches file by @francorisso in #3146 +* Fix - RolloutSwitches Init by @carterbuce in #3157 +* Add - Integrate Whatsapp Utility Messaging for WooCommerce Order Update Notifications by @sharunaanandraj in #3164 +* Tweak - Improve Test Filter Management with AbstractWPUnitTestWithSafeFiltering by @sol-loup in #2944 +* Fix - Namespacing issue causing some tests to be skipped @sol-loup in #3037 +* Tweak - Additional logs and timeout for Utility Message Flows by @woo-ardsouza in #3171 +* Fix - The WAUM payment progress to only Show Up after Consent Collection is Enabled by @sharunaanandraj in #3175 +* Tweak - Update language dropdown based on supported_languages in GET api response by @woo-ardsouza in #3178 +* Add - Error notice to gracefully handle errors in Manage Events view by @woo-ardsouza in #3179 +* Fix - The Status on the Whatsapp Consent Collection Pill and Button @sharunaanandraj in #3183 +* Tweak - Update Message Sending API from Messages to Message Events by @woo-ardsouza in #3182 +* Tweak - Update the Authentication mechanism for Whatsapp Webhook by @sharunaanandraj #3186 +* Tweak - Minor design updates to Utility Event Settings card by @woo-ardsouza in #3193 +* Add - Admin notice for WhatsApp utility messaging recruitment @iodic in #3177 +* Fix - The product sync button showing up twice by @sharunaanandraj in #3199 +* Tweak - Bump WooCommerce and WordPress compatibility by @iodic in #3200 +* Add - An automated process that synchronizes all WooCommerce product categories with Meta, creating catalog product sets for each category. The synchronization process ensures that any changes made to the WooCommerce product categories are reflected in the corresponding Meta catalog product sets by @mshymon in #3168 +* Add - A banner in product sets tab to explain recent changes to product sets sync by @mshymon in #3207 +* Add - Admin notice for WhatsApp utility messaging recruitment by @iodic in #3211 [See changelog for all versions](https://raw.githubusercontent.com/facebook/facebook-for-woocommerce/refs/heads/releases/changelog.txt). diff --git a/tests/Unit/AbstractWPUnitTestWithSafeFiltering.php b/tests/Unit/AbstractWPUnitTestWithSafeFiltering.php new file mode 100644 index 000000000..fe9f11b89 --- /dev/null +++ b/tests/Unit/AbstractWPUnitTestWithSafeFiltering.php @@ -0,0 +1,125 @@ +filter_callbacks = []; + } + + /** + * Clean up after each test. + */ + public function tearDown(): void { + // Remove specific filters that were added by this test + foreach ($this->filter_callbacks as $hook => $callbacks) { + foreach ($callbacks as $callback_data) { + remove_filter($hook, $callback_data['callback'], $callback_data['priority']); + } + } + + parent::tearDown(); + } + + /** + * Helper method to remove all filters for a specific hook safely + * + * @param string $hook The filter hook name to remove all callbacks for + * @return void + */ + protected function teardown_callback_category_safely($hook) { + if (isset($this->filter_callbacks[$hook])) { + foreach ($this->filter_callbacks[$hook] as $callback_data) { + remove_filter($hook, $callback_data['callback'], $callback_data['priority']); + } + // Clear the tracking for this hook + unset($this->filter_callbacks[$hook]); + } + } + + /** + * Helper method to add a filter and store its callback for later removal + * + * @param string $hook The filter hook name + * @param callable $callback The filter callback function + * @param int $priority The priority of the filter + * @param int $accepted_args The number of arguments the function accepts + * @return object A simple object with remove() method for easy cleanup + */ + protected function add_filter_with_safe_teardown($hook, $callback, $priority = 10, $accepted_args = 1) { + add_filter($hook, $callback, $priority, $accepted_args); + + if (!isset($this->filter_callbacks[$hook])) { + $this->filter_callbacks[$hook] = []; + } + + $this->filter_callbacks[$hook][] = [ + 'callback' => $callback, + 'priority' => $priority + ]; + + $self = $this; + + // Return a simple object with a remove method + return new class($hook, $callback, $priority, $self) { + private $hook; + private $callback; + private $priority; + private $test_case; + + public function __construct($hook, $callback, $priority, $test_case) { + $this->hook = $hook; + $this->callback = $callback; + $this->priority = $priority; + $this->test_case = $test_case; + } + + public function teardown_safely_immediately() { + remove_filter($this->hook, $this->callback, $this->priority); + $this->test_case->removeFilterFromTracking($this->hook, $this->callback, $this->priority); + } + }; + } + + /** + * Remove a filter from the tracking array + * + * @param string $hook The filter hook name + * @param callable $callback The filter callback function + * @param int $priority The priority of the filter + */ + public function removeFilterFromTracking($hook, $callback, $priority) { + if (isset($this->filter_callbacks[$hook])) { + foreach ($this->filter_callbacks[$hook] as $key => $callback_data) { + if ($callback_data['callback'] === $callback && $callback_data['priority'] === $priority) { + unset($this->filter_callbacks[$hook][$key]); + break; + } + } + + // Clean up empty arrays + if (empty($this->filter_callbacks[$hook])) { + unset($this->filter_callbacks[$hook]); + } + } + } +} diff --git a/tests/Unit/Admin/Settings/ConnectionTest.php b/tests/Unit/Admin/Settings/ConnectionTest.php index 18a94e4d2..b525ca3f0 100644 --- a/tests/Unit/Admin/Settings/ConnectionTest.php +++ b/tests/Unit/Admin/Settings/ConnectionTest.php @@ -1,6 +1,6 @@ add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->get_catalog( '726635365295186' ); @@ -78,7 +78,7 @@ public function test_perform_request_produces_wp_error() { $this->assertEquals( "{$this->endpoint}{$this->version}/726635365295186?fields=name", $url ); return new WP_Error( 007, 'WP Error Message' ); }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $this->api->get_catalog( '726635365295186' ); } @@ -103,7 +103,7 @@ public function test_get_installation_ids_returns_installation_ids_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->get_installation_ids( $external_business_id ); @@ -130,7 +130,7 @@ public function test_get_catalog_returns_catalog_id_and_name_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->get_catalog( $catalog_id ); @@ -158,7 +158,7 @@ public function test_get_user_returns_user_information_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->get_user( $user_id ); @@ -187,7 +187,7 @@ public function test_delete_mbe_connection_deletes_mbe_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->delete_mbe_connection( $external_business_id ); @@ -214,7 +214,7 @@ public function test_get_business_configuration_returns_business_configuration_r ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->get_business_configuration( $external_business_id ); @@ -267,7 +267,7 @@ public function test_send_item_updates_sends_item_updates_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->send_item_updates( $facebook_catalog_id, $requests ); @@ -313,7 +313,7 @@ public function test_create_product_group_performs_create_product_group_request( ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->create_product_group( $facebook_product_catalog_id, $data ); @@ -355,7 +355,7 @@ public function test_update_product_group_preforms_update_product_group_request( ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->update_product_group( $facebook_product_group_id, $data ); @@ -382,7 +382,7 @@ public function test_delete_product_group_deletes_product_group_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->delete_product_group( $facebook_product_group_id ); @@ -410,7 +410,7 @@ public function test_get_product_group_products_returns_group_products_request() ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->get_product_group_products( $facebook_product_group_id, $limit ); @@ -473,7 +473,7 @@ public function test_create_product_item_creates_product_item_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->create_product_item( $facebook_product_group_id, $data ); @@ -520,7 +520,7 @@ public function test_update_product_item_updated_product_item_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->update_product_item( $facebook_product_id, $data ); @@ -553,7 +553,7 @@ public function test_get_product_facebook_ids_creates_get_ids_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->get_product_facebook_ids( $facebook_product_catalog_id, $facebook_product_retailer_id ); @@ -668,7 +668,7 @@ public function test_delete_product_item_deletes_product_item_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->delete_product_item( $facebook_product_id ); @@ -701,7 +701,7 @@ public function test_create_product_set_item_creates_set_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->create_product_set_item( $facebook_product_catalog_id, $data ); @@ -734,7 +734,7 @@ public function test_update_product_set_item_updates_set_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->update_product_set_item( $facebook_product_set_id, $data ); @@ -761,7 +761,7 @@ public function test_delete_product_set_item_deletes_set_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->delete_product_set_item( $facebook_product_set_id, true ); @@ -788,7 +788,7 @@ public function test_read_feeds_creates_read_feeds_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->read_feeds( $facebook_product_catalog_id ); @@ -828,7 +828,7 @@ public function test_create_feed_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response_feed = $this->api->create_feed( $facebook_product_catalog_id, $data ); @@ -858,9 +858,36 @@ public function test_create_upload_request() { ], ]; }; - add_filter( 'pre_http_request', $response, 10, 3 ); + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); $response = $this->api->create_upload( $product_feed_id, $data ); $this->assertFalse( $response->has_api_error() ); } + + /** + * Tests read product set request to Facebook. + * + * @return void + * @throws ApiException In case of network request error. + */ + public function test_read_product_set_item() { + $product_catalog_id = '726635365295186'; + $retailer_id = '29'; + + $response = function( $result, $parsed_args, $url ) use ( $product_catalog_id, $retailer_id ) { + $this->assertEquals( 'GET', $parsed_args['method'] ); + $this->assertEquals( "{$this->endpoint}{$this->version}/{$product_catalog_id}/product_sets?retailer_id={$retailer_id}", $url ); + return [ + 'body' => '{"id":"325235346346546"}', + 'response' => [ + 'code' => 200, + 'message' => 'OK', + ], + ]; + }; + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); + + $response = $this->api->read_product_set_item( $product_catalog_id, $retailer_id ); + $this->assertFalse( $response->has_api_error() ); + } } diff --git a/tests/Unit/RolloutSwitchesTest.php b/tests/Unit/RolloutSwitchesTest.php new file mode 100644 index 000000000..4e7953a86 --- /dev/null +++ b/tests/Unit/RolloutSwitchesTest.php @@ -0,0 +1,130 @@ +api = new Api( $this->access_token ); + } + + public function test_api() { + $response = function( $result, $parsed_args, $url ) { + $this->assertEquals( 'GET', $parsed_args['method'] ); + $url_params = "access_token={$this->access_token}&fbe_external_business_id={$this->external_business_id}"; + $path = "fbe_business/fbe_rollout_switches"; + $this->assertEquals( "{$this->endpoint}{$this->version}/{$path}?{$url_params}", $url ); + return [ + 'body' => '{"data":[{"switch": "switch_a","enabled":"1"}, {"switch": "switch_b","enabled":""}, {"switch": "switch_c","enabled":"1"}]}', + 'response' => [ + 'code' => 200, + 'message' => 'OK', + ], + ]; + }; + $this->add_filter_with_safe_teardown( 'pre_http_request', $response, 10, 3 ); + + $response = $this->api->get_rollout_switches( $this->external_business_id ); + $this->assertEquals([ + [ + 'switch' => 'switch_a', + 'enabled' => '1', + ], + [ + 'switch' => 'switch_b', + 'enabled' => '', + ], + [ + 'switch' => 'switch_c', + 'enabled' => '1', + ], + ], $response->get_data()); + } + + public function test_plugin() { + + // mock the active filters to test business values + $plugin = facebook_for_woocommerce(); + $plugin_ref_obj = new ReflectionObject( $plugin ); + // setup connection handler + $prop_connection_handler = $plugin_ref_obj->getProperty( 'connection_handler' ); + $prop_connection_handler->setAccessible( true ); + $mock_connection_handler = $this->getMockBuilder( Connection::class ) + ->disableOriginalConstructor() + ->setMethods( array( 'get_external_business_id', 'is_connected', 'get_access_token' ) ) + ->getMock(); + $mock_connection_handler->expects( $this->any() )->method( 'get_external_business_id' )->willReturn( $this->external_business_id ); + $mock_connection_handler->expects( $this->any() )->method( 'get_access_token' )->willReturn( $this->access_token ); + $mock_connection_handler->expects( $this->any() )->method( 'is_connected' )->willReturn( true ); + $prop_connection_handler->setValue( $plugin, $mock_connection_handler ); + // setup API + $prop_api = $plugin_ref_obj->getProperty( 'api' ); + $prop_api->setAccessible( true ); + $mock_api = $this->getMockBuilder( API::class )->disableOriginalConstructor()->setMethods( array( 'do_remote_request' ) )->getMock(); + $mock_api->expects( $this->any() )->method( 'do_remote_request' )->willReturn( + array('body' => json_encode(array( + 'data' => array( + array('switch' => 'switch_a','enabled' => '1'), + array('switch' => 'switch_b', 'enabled' => ''), + array( 'switch' => 'switch_c', 'enabled' => '1'), + ) + )))); + $prop_api->setValue( $plugin, $mock_api ); + + $switch_mock = $this->getMockBuilder(RolloutSwitches::class) + ->setConstructorArgs( array( $plugin ) ) + ->onlyMethods(['is_switch_active']) + ->getMock(); + $switch_mock->expects($this->any())->method('is_switch_active') + ->willReturnCallback(function($switch_name) { + switch ($switch_name) { + case 'switch_a': + case 'switch_b': + case 'switch_d': + return true; + default: + return false; + } + }); + $switch_mock->init(); + + // If the switch is not active -> FALSE (independent of the response being true) + $this->assertEquals( $switch_mock->is_switch_enabled("switch_c"), false ); + + // If the feature is active and in the response -> response value + $this->assertEquals( $switch_mock->is_switch_enabled("switch_a"), true ); + $this->assertEquals( $switch_mock->is_switch_enabled("switch_b"), false ); + + // If the switch is active but not in the response -> TRUE + $this->assertEquals( $switch_mock->is_switch_enabled("switch_d"), true ); + } +} diff --git a/tests/Unit/WCFacebookCommerceIntegrationTest.php b/tests/Unit/WCFacebookCommerceIntegrationTest.php index 2757b33b6..5e443a589 100644 --- a/tests/Unit/WCFacebookCommerceIntegrationTest.php +++ b/tests/Unit/WCFacebookCommerceIntegrationTest.php @@ -16,7 +16,7 @@ /** * Unit tests for Facebook Graph API calls. */ -class WCFacebookCommerceIntegrationTest extends WP_UnitTestCase { +class WCFacebookCommerceIntegrationTest extends \WooCommerce\Facebook\Tests\AbstractWPUnitTestWithSafeFiltering { /** * @var WC_Facebookcommerce @@ -151,7 +151,7 @@ public function test_init_pixel_for_admin_user_must_init_pixel_overwrites_pixel_ self::$default_options ); - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_pixel_id', function ( $wc_facebook_pixel_id ) { return '998877665544332211'; @@ -189,7 +189,7 @@ public function test_init_pixel_for_admin_user_must_init_pixel_overwrites_use_pi self::$default_options ); - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_is_advanced_matching_enabled', function ( $use_pii ) { return false; @@ -329,7 +329,7 @@ public function test_get_variation_product_item_ids_from_facebook_with_fb_retail ->with( $facebook_product_group_id ) ->willReturn( $facebook_response ); - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_fb_retailer_id', function ( $retailer_id ) { return $retailer_id . '_modified'; @@ -368,7 +368,7 @@ public function test_get_product_count_returns_product_count_with_no_filters() { * @return void */ public function test_get_product_count_returns_product_count_with_filters() { - add_filter( + $this->add_filter_with_safe_teardown( 'wp_count_posts', function( $counts ) { $counts->publish = 21; @@ -402,7 +402,7 @@ public function test_allow_full_batch_api_sync_returns_default_allow_status_with * @return void */ public function test_allow_full_batch_api_sync_uses_block_full_batch_api_sync_filter() { - add_filter( + $this->add_filter_with_safe_teardown( 'facebook_for_woocommerce_block_full_batch_api_sync', function ( bool $status ) { return true; @@ -425,7 +425,7 @@ function ( bool $status ) { public function test_allow_full_batch_api_sync_uses_allow_full_batch_api_sync_filter() { $this->markTestSkipped( 'Some problems with phpunit polyfills notices handling.' ); - add_filter( + $this->add_filter_with_safe_teardown( 'facebook_for_woocommerce_allow_full_batch_api_sync', function ( bool $status ) { return false; @@ -556,7 +556,7 @@ public function test_on_product_save_existing_simple_product_sync_enabled_update // Verify Facebook-specific fields were saved $facebook_product_to_update = new WC_Facebook_Product( $product_to_update->get_id() ); $updated_product_data = $facebook_product_to_update->prepare_product(null, \WC_Facebook_Product::PRODUCT_PREP_TYPE_ITEMS_BATCH ); - + $this->assertEquals( 'Facebook product description.', get_post_meta( $facebook_product_to_update->get_id(), WC_Facebook_Product::FB_PRODUCT_DESCRIPTION, true ) ); $this->assertEquals( 'Facebook product description.', get_post_meta( $facebook_product_to_update->get_id(), WC_Facebook_Product::FB_RICH_TEXT_DESCRIPTION, true ) ); @@ -1953,7 +1953,7 @@ public function test_reset_single_product_for_variable_product() { */ public function test_get_product_catalog_id_returns_product_catalog_from_initialised_property_using_no_filter() { $this->integration->product_catalog_id = '123123123123123123'; - remove_all_filters( 'wc_facebook_product_catalog_id' ); + $this->teardown_callback_category_safely( 'wc_facebook_product_catalog_id' ); $product_catalog_id = $this->integration->get_product_catalog_id(); @@ -1968,7 +1968,7 @@ public function test_get_product_catalog_id_returns_product_catalog_from_initial public function test_get_product_catalog_id_returns_product_catalog_from_options_using_no_filter() { $this->integration->product_catalog_id = null; add_option( WC_Facebookcommerce_Integration::OPTION_PRODUCT_CATALOG_ID, '321321321321321321' ); - remove_all_filters( 'wc_facebook_product_catalog_id' ); + $this->teardown_callback_category_safely( 'wc_facebook_product_catalog_id' ); $product_catalog_id = $this->integration->get_product_catalog_id(); @@ -1982,7 +1982,7 @@ public function test_get_product_catalog_id_returns_product_catalog_from_options * @return void */ public function test_get_product_catalog_id_returns_product_catalog_with_filter() { - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_product_catalog_id', function ( $product_catalog_id ) { return '3213-2132-1321-3213-2132'; @@ -2001,7 +2001,7 @@ function ( $product_catalog_id ) { */ public function test_get_external_merchant_settings_id_returns_settings_id_from_initialised_property_using_no_filter() { $this->integration->external_merchant_settings_id = '123123123123123123'; - remove_all_filters( 'wc_facebook_external_merchant_settings_id' ); + $this->teardown_callback_category_safely( 'wc_facebook_external_merchant_settings_id' ); $external_merchant_settings_id = $this->integration->get_external_merchant_settings_id(); @@ -2016,7 +2016,7 @@ public function test_get_external_merchant_settings_id_returns_settings_id_from_ public function test_get_external_merchant_settings_id_returns_settings_id_from_options_using_no_filter() { $this->integration->external_merchant_settings_id = null; add_option( WC_Facebookcommerce_Integration::OPTION_EXTERNAL_MERCHANT_SETTINGS_ID, '321321321321321321' ); - remove_all_filters( 'wc_facebook_external_merchant_settings_id' ); + $this->teardown_callback_category_safely( 'wc_facebook_external_merchant_settings_id' ); $external_merchant_settings_id = $this->integration->get_external_merchant_settings_id(); @@ -2030,7 +2030,7 @@ public function test_get_external_merchant_settings_id_returns_settings_id_from_ * @return void */ public function test_get_external_merchant_settings_id_returns_settings_id_with_filter() { - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_external_merchant_settings_id', function ( $external_merchant_settings_id ) { return '3213-2132-1321-3213-2132'; @@ -2049,7 +2049,7 @@ function ( $external_merchant_settings_id ) { */ public function test_get_feed_id_returns_id_from_initialised_property_using_no_filter() { $this->integration->feed_id = '123123123123123123'; - remove_all_filters( 'wc_facebook_feed_id' ); + $this->teardown_callback_category_safely( 'wc_facebook_feed_id' ); $feed_id = $this->integration->get_feed_id(); @@ -2064,7 +2064,7 @@ public function test_get_feed_id_returns_id_from_initialised_property_using_no_f public function test_get_feed_id_returns_id_from_options_using_no_filter() { $this->integration->feed_id = null; add_option( WC_Facebookcommerce_Integration::OPTION_FEED_ID, '321321321321321321' ); - remove_all_filters( 'wc_facebook_feed_id' ); + $this->teardown_callback_category_safely( 'wc_facebook_feed_id' ); $feed_id = $this->integration->get_feed_id(); @@ -2078,7 +2078,7 @@ public function test_get_feed_id_returns_id_from_options_using_no_filter() { * @return void */ public function test_get_feed_id_returns_id_with_filter() { - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_feed_id', function ( $feed_id ) { return '3213-2132-1321-3213-2132'; @@ -2097,7 +2097,7 @@ function ( $feed_id ) { */ public function test_get_upload_id_returns_id_from_options_using_no_filter() { add_option( WC_Facebookcommerce_Integration::OPTION_UPLOAD_ID, '321321321321321321' ); - remove_all_filters( 'wc_facebook_upload_id' ); + $this->teardown_callback_category_safely( 'wc_facebook_upload_id' ); $upload_id = $this->integration->get_upload_id(); @@ -2110,7 +2110,7 @@ public function test_get_upload_id_returns_id_from_options_using_no_filter() { * @return void */ public function test_get_upload_id_returns_id_with_filter() { - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_upload_id', function ( $upload_id ) { return '3213-2132-1321-3213-2132'; @@ -2129,7 +2129,7 @@ function ( $upload_id ) { */ public function test_get_pixel_install_time_returns_id_from_initialised_property_using_no_filter() { $this->integration->pixel_install_time = '123123123123123123'; - remove_all_filters( 'wc_facebook_pixel_install_time' ); + $this->teardown_callback_category_safely( 'wc_facebook_pixel_install_time' ); $pixel_install_time = $this->integration->get_pixel_install_time(); @@ -2144,7 +2144,7 @@ public function test_get_pixel_install_time_returns_id_from_initialised_property public function test_get_pixel_install_time_returns_id_from_options_using_no_filter() { $this->integration->pixel_install_time = null; add_option( WC_Facebookcommerce_Integration::OPTION_PIXEL_INSTALL_TIME, '321321321321321321' ); - remove_all_filters( 'wc_facebook_pixel_install_time' ); + $this->teardown_callback_category_safely( 'wc_facebook_pixel_install_time' ); $pixel_install_time = $this->integration->get_pixel_install_time(); @@ -2158,7 +2158,7 @@ public function test_get_pixel_install_time_returns_id_from_options_using_no_fil * @return void */ public function test_get_pixel_install_time_returns_id_with_filter() { - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_pixel_install_time', function ( $pixel_install_time ) { return '321321321321'; @@ -2179,7 +2179,7 @@ public function test_get_js_sdk_version_returns_id_from_options_using_no_filter( $this->markTestSkipped( 'get_js_sdk_version method is called in constructor which makes it impossible to test it in isolation w/o refactoring the constructor.' ); add_option( WC_Facebookcommerce_Integration::OPTION_JS_SDK_VERSION, 'v1.0.0' ); - remove_all_filters( 'wc_facebook_js_sdk_version' ); + $this->teardown_callback_category_safely( 'wc_facebook_js_sdk_version' ); $js_sdk_version = $this->integration->get_js_sdk_version(); @@ -2194,7 +2194,7 @@ public function test_get_js_sdk_version_returns_id_from_options_using_no_filter( public function test_get_js_sdk_version_returns_id_with_filter() { $this->markTestSkipped( 'get_js_sdk_version method is called in constructor which makes it impossible to test it in isolation w/o refactoring the constructor.' ); - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_js_sdk_version', function ( $js_sdk_version ) { return 'v2.0.0'; @@ -2212,7 +2212,7 @@ function ( $js_sdk_version ) { * @return void */ public function test_get_facebook_page_id_no_filters() { - remove_all_filters( 'wc_facebook_page_id' ); + $this->teardown_callback_category_safely( 'wc_facebook_page_id' ); add_option( WC_Facebookcommerce_Integration::SETTING_FACEBOOK_PAGE_ID, '222333111444555666777' ); $facebook_page_id = $this->integration->get_facebook_page_id(); @@ -2226,7 +2226,7 @@ public function test_get_facebook_page_id_no_filters() { * @return void */ public function test_get_facebook_page_id_with_filter() { - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_page_id', function ( $facebook_page_id ) { return '444333222111999888777666555'; @@ -2244,7 +2244,7 @@ function ( $facebook_page_id ) { * @return void */ public function test_get_facebook_pixel_id_no_filters() { - remove_all_filters( 'wc_facebook_pixel_id' ); + $this->teardown_callback_category_safely( 'wc_facebook_pixel_id' ); add_option( WC_Facebookcommerce_Integration::SETTING_FACEBOOK_PIXEL_ID, '222333111444555666777' ); $facebook_pixel_id = $this->integration->get_facebook_pixel_id(); @@ -2258,7 +2258,7 @@ public function test_get_facebook_pixel_id_no_filters() { * @return void */ public function test_get_facebook_pixel_id_with_filter() { - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_pixel_id', function ( $facebook_pixel_id ) { return '444333222111999888777666555'; @@ -2276,7 +2276,7 @@ function ( $facebook_pixel_id ) { * @return void */ public function test_get_excluded_product_category_ids_no_filter_no_option() { - remove_all_filters( 'wc_facebook_excluded_product_category_ids' ); + $this->teardown_callback_category_safely( 'wc_facebook_excluded_product_category_ids' ); delete_option( WC_Facebookcommerce_Integration::SETTING_EXCLUDED_PRODUCT_CATEGORY_IDS ); $categories = $this->integration->get_excluded_product_category_ids(); @@ -2290,7 +2290,7 @@ public function test_get_excluded_product_category_ids_no_filter_no_option() { * @return void */ public function test_get_excluded_product_category_ids_no_filter() { - remove_all_filters( 'wc_facebook_excluded_product_category_ids' ); + $this->teardown_callback_category_safely( 'wc_facebook_excluded_product_category_ids' ); add_option( WC_Facebookcommerce_Integration::SETTING_EXCLUDED_PRODUCT_CATEGORY_IDS, [ 121, 221, 321, 421, 521, 621 ] @@ -2307,7 +2307,7 @@ public function test_get_excluded_product_category_ids_no_filter() { * @return void */ public function test_get_excluded_product_category_ids_with_filter() { - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_excluded_product_category_ids', function ( $ids ) { return [ 111, 222, 333 ]; @@ -2330,7 +2330,7 @@ function ( $ids ) { * @return void */ public function test_get_excluded_product_tag_ids_no_filter_no_option() { - remove_all_filters( 'wc_facebook_excluded_product_tag_ids' ); + $this->teardown_callback_category_safely( 'wc_facebook_excluded_product_tag_ids' ); delete_option( WC_Facebookcommerce_Integration::SETTING_EXCLUDED_PRODUCT_TAG_IDS ); $tags = $this->integration->get_excluded_product_tag_ids(); @@ -2344,7 +2344,7 @@ public function test_get_excluded_product_tag_ids_no_filter_no_option() { * @return void */ public function test_get_excluded_product_tag_ids_no_filter() { - remove_all_filters( 'wc_facebook_excluded_product_tag_ids' ); + $this->teardown_callback_category_safely( 'wc_facebook_excluded_product_tag_ids' ); add_option( WC_Facebookcommerce_Integration::SETTING_EXCLUDED_PRODUCT_TAG_IDS, [ 121, 221, 321, 421, 521, 621 ] @@ -2361,7 +2361,7 @@ public function test_get_excluded_product_tag_ids_no_filter() { * @return void */ public function test_get_excluded_product_tag_ids_with_filter() { - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_excluded_product_tag_ids', function ( $ids ) { return [ 111, 222, 333 ]; @@ -2379,6 +2379,7 @@ function ( $ids ) { } + /** * Tests product catalog id option update with valid catalog id value. * @@ -2595,7 +2596,7 @@ public function test_is_configured_returns_false_is_not_connected() { * @return void */ public function test_is_advanced_matching_enabled_no_filter() { - remove_all_filters( 'wc_facebook_is_advanced_matching_enabled' ); + $this->teardown_callback_category_safely( 'wc_facebook_is_advanced_matching_enabled' ); $output = $this->integration->is_advanced_matching_enabled(); @@ -2608,7 +2609,7 @@ public function test_is_advanced_matching_enabled_no_filter() { * @return void */ public function test_is_advanced_matching_enabled_with_filter() { - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_is_advanced_matching_enabled', function ( $is_enabled ) { return false; @@ -2626,7 +2627,7 @@ function ( $is_enabled ) { * @return void */ public function test_is_product_sync_enabled_no_filter_no_option() { - remove_all_filters( 'wc_facebook_is_product_sync_enabled' ); + $this->teardown_callback_category_safely( 'wc_facebook_is_product_sync_enabled' ); delete_option( WC_Facebookcommerce_Integration::SETTING_ENABLE_PRODUCT_SYNC ); $result = $this->integration->is_product_sync_enabled(); @@ -2640,7 +2641,7 @@ public function test_is_product_sync_enabled_no_filter_no_option() { * @return void */ public function test_is_product_sync_enabled_no_filter() { - remove_all_filters( 'wc_facebook_is_product_sync_enabled' ); + $this->teardown_callback_category_safely( 'wc_facebook_is_product_sync_enabled' ); add_option( WC_Facebookcommerce_Integration::SETTING_ENABLE_PRODUCT_SYNC, 'no' @@ -2657,7 +2658,7 @@ public function test_is_product_sync_enabled_no_filter() { * @return void */ public function test_is_product_sync_enabled_with_filter() { - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_is_product_sync_enabled', function ( $is_enabled ) { return false; @@ -2708,7 +2709,7 @@ public function test_is_legacy_feed_file_generation_enabled_with_option() { * @return void */ public function test_is_debug_mode_enabled_returns_default_value() { - remove_all_filters( 'wc_facebook_is_debug_mode_enabled' ); + $this->teardown_callback_category_safely( 'wc_facebook_is_debug_mode_enabled' ); delete_option( WC_Facebookcommerce_Integration::SETTING_ENABLE_DEBUG_MODE ); $result = $this->integration->is_debug_mode_enabled(); @@ -2722,7 +2723,7 @@ public function test_is_debug_mode_enabled_returns_default_value() { * @return void */ public function test_is_debug_mode_enabled_returns_option_value() { - remove_all_filters( 'wc_facebook_is_debug_mode_enabled' ); + $this->teardown_callback_category_safely( 'wc_facebook_is_debug_mode_enabled' ); add_option( WC_Facebookcommerce_Integration::SETTING_ENABLE_DEBUG_MODE, 'yes' @@ -2739,7 +2740,7 @@ public function test_is_debug_mode_enabled_returns_option_value() { * @return void */ public function test_is_debug_mode_enabled_with_filter() { - add_filter( + $this->add_filter_with_safe_teardown( 'wc_facebook_is_debug_mode_enabled', function ( $is_enabled ) { return false; diff --git a/tests/Unit/WPMLTest.php b/tests/Unit/WPMLTest.php index 436e70003..79931c7a6 100644 --- a/tests/Unit/WPMLTest.php +++ b/tests/Unit/WPMLTest.php @@ -1,6 +1,6 @@ add_filter_with_safe_teardown( 'wpml_post_language_details', function() { return new WP_Error(); @@ -36,7 +37,7 @@ function() { public function test_product_hidden_no_settings_and_not_default() { WC_Facebook_WPML_Injector::$default_lang = 'en_US'; - add_filter( + $filter = $this->add_filter_with_safe_teardown( 'wpml_post_language_details', function() { return [ @@ -50,7 +51,7 @@ function() { public function test_product_not_hidden_no_settings_and_default() { WC_Facebook_WPML_Injector::$default_lang = 'en_US'; - add_filter( + $filter = $this->add_filter_with_safe_teardown( 'wpml_post_language_details', function() { return [ @@ -67,7 +68,7 @@ public function test_product_hidden_language_setting_not_visible() { 'fr_FR' => FB_WPML_Language_Status::HIDDEN, ]; - add_filter( + $filter = $this->add_filter_with_safe_teardown( 'wpml_post_language_details', function() { return [ @@ -84,7 +85,7 @@ public function test_product_not_hidden_language_setting_visible() { 'fr_FR' => FB_WPML_Language_Status::VISIBLE, ]; - add_filter( + $filter = $this->add_filter_with_safe_teardown( 'wpml_post_language_details', function() { return [ diff --git a/tests/Unit/fbproductTest.php b/tests/Unit/fbproductTest.php index a2637ec70..1f42f7f05 100644 --- a/tests/Unit/fbproductTest.php +++ b/tests/Unit/fbproductTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -class fbproductTest extends WP_UnitTestCase { +class fbproductTest extends \WooCommerce\Facebook\Tests\AbstractWPUnitTestWithSafeFiltering { private $parent_fb_product; /** @var \WC_Product_Simple */ @@ -104,18 +104,18 @@ public function test_filter_fb_description() { $facebook_product = new \WC_Facebook_Product( $product ); $facebook_product->set_description( 'fb description' ); - add_filter( 'facebook_for_woocommerce_fb_product_description', function( $description ) { + $filter = $this->add_filter_with_safe_teardown( 'facebook_for_woocommerce_fb_product_description', function( $description ) { return 'filtered description'; }); $description = $facebook_product->get_fb_description(); $this->assertEquals( $description, 'filtered description' ); - remove_all_filters( 'facebook_for_woocommerce_fb_product_description' ); + // Remove the filter early + $filter->teardown_safely_immediately(); $description = $facebook_product->get_fb_description(); $this->assertEquals( $description, 'fb description' ); - } /** @@ -653,15 +653,16 @@ public function test_get_rich_text_description() { $this->assertEquals('

short description test

', $description); // Test 7: Applies filters - add_filter('facebook_for_woocommerce_fb_rich_text_description', function($description) { + $filter = $this->add_filter_with_safe_teardown('facebook_for_woocommerce_fb_rich_text_description', function($description) { return '

filtered description

'; }); $description = $facebook_product->get_rich_text_description(); $this->assertEquals('

filtered description

', $description); - // Cleanup - remove_all_filters('facebook_for_woocommerce_fb_rich_text_description'); + // Remove the filter early + $filter->teardown_safely_immediately(); + delete_option(WC_Facebookcommerce_Integration::SETTING_PRODUCT_DESCRIPTION_MODE); } diff --git a/webpack.config.js b/webpack.config.js index 080f4ae1c..3949fc7e7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,23 +12,31 @@ const jQueryUIAdminFileNames = [ 'products-admin', 'settings-commerce', 'settings-sync', + 'whatsapp-billing', + 'whatsapp-connection', + 'whatsapp-consent', + 'whatsapp-templates', + 'whatsapp-finish', + 'whatsapp-consent-remove', + 'whatsapp-disconnect', + 'whatsapp-events', ]; const jQueryUIAdminFileEntries = {}; jQueryUIAdminFileNames.forEach( ( name ) => { - jQueryUIAdminFileEntries[ `admin/${ name }` ] = `./assets/js/admin/${ name }.js`; + jQueryUIAdminFileEntries[ `admin/${ name }` ] = `./assets/js/admin/${ name }.js`; } ); module.exports = { - ...defaultConfig, - entry: { - // Use admin/index.js for any new React-powered UI - 'admin/index': './assets/js/admin/index.js', - ...jQueryUIAdminFileEntries, - }, - output: { - filename: '[name].js', - path: __dirname + '/assets/build', - }, + ...defaultConfig, + entry: { + // Use admin/index.js for any new React-powered UI + 'admin/index': './assets/js/admin/index.js', + ...jQueryUIAdminFileEntries, + }, + output: { + filename: '[name].js', + path: __dirname + '/assets/build', + }, };