diff --git a/.github/ISSUE_TEMPLATE/e2e_test_report.md b/.github/ISSUE_TEMPLATE/e2e_test_report.md new file mode 100644 index 00000000000..4eebc0748bb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/e2e_test_report.md @@ -0,0 +1,39 @@ +--- +name: e2e test report +about: Report an e2e test failure. +title: '' +labels: ['category: e2e', 'needs triage'] +assignees: '' + +--- + +### Description + + + + +**Output from the test failure**: + + +### Additional context + + + +### Priority + + + +### Reason why this e2e test is broken + + + +> [!Important] +> Please, ensure when closing this issue (PR fix) that only one `e2e: broken` label is added and it is accurate. +> - [ ] I confirmed there's only one `e2e: broken` label in this issue and it is accurate. + diff --git a/changelog/add-account-reset-for-sandboxes b/changelog/add-account-reset-for-sandboxes new file mode 100644 index 00000000000..8b02862796f --- /dev/null +++ b/changelog/add-account-reset-for-sandboxes @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add account reset for sandboxes diff --git a/changelog/add-woopay-merchant-request b/changelog/add-woopay-merchant-request new file mode 100644 index 00000000000..621528f4912 --- /dev/null +++ b/changelog/add-woopay-merchant-request @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Allow WooPay to request full session data from store. diff --git a/changelog/chore-e2e-test-report-template b/changelog/chore-e2e-test-report-template new file mode 100644 index 00000000000..0f8f73cc671 --- /dev/null +++ b/changelog/chore-e2e-test-report-template @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Create e2e test report issue template. + + diff --git a/changelog/dev-4710-remove-deposit-status b/changelog/dev-4710-remove-deposit-status deleted file mode 100644 index 871d531baf0..00000000000 --- a/changelog/dev-4710-remove-deposit-status +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Removed deprecated deposit_status key from account status diff --git a/changelog/fix-cursor-pointer-for-disabled-logging b/changelog/fix-cursor-pointer-for-disabled-logging new file mode 100644 index 00000000000..8688da18944 --- /dev/null +++ b/changelog/fix-cursor-pointer-for-disabled-logging @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix the cursor pointer when hovering over disabled checkboxes in Advanced Settings diff --git a/changelog/revert-8376-removed-depositsstatus b/changelog/revert-8376-removed-depositsstatus new file mode 100644 index 00000000000..f734c4db749 --- /dev/null +++ b/changelog/revert-8376-removed-depositsstatus @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Reverting https://github.com/Automattic/woocommerce-payments/pull/8376 that removes depositsStatus key from account response. + + diff --git a/client/checkout/woopay/direct-checkout/index.js b/client/checkout/woopay/direct-checkout/index.js index 86c8d9995e5..ef7fcc04388 100644 --- a/client/checkout/woopay/direct-checkout/index.js +++ b/client/checkout/woopay/direct-checkout/index.js @@ -26,14 +26,14 @@ window.addEventListener( 'load', async () => { if ( isThirdPartyCookieEnabled ) { if ( await WooPayDirectCheckout.isUserLoggedIn() ) { WooPayDirectCheckout.maybePrefetchEncryptedSessionData(); - WooPayDirectCheckout.redirectToWooPay( checkoutElements ); + WooPayDirectCheckout.redirectToWooPay( checkoutElements, true ); } return; } - // Pass true to append '&checkout_redirect=1' and let WooPay decide the checkout flow. - WooPayDirectCheckout.redirectToWooPay( checkoutElements, true ); + // Pass false to indicate we are not sure if the user is logged in or not. + WooPayDirectCheckout.redirectToWooPay( checkoutElements, false ); } ); jQuery( ( $ ) => { diff --git a/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js b/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js index edd327d9645..40321a32545 100644 --- a/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js +++ b/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js @@ -6,6 +6,7 @@ import request from 'wcpay/checkout/utils/request'; import { buildAjaxURL } from 'wcpay/payment-request/utils'; import UserConnect from 'wcpay/checkout/woopay/connect/user-connect'; import SessionConnect from 'wcpay/checkout/woopay/connect/session-connect'; +import { getTracksIdentity } from 'tracks'; /** * The WooPayDirectCheckout class is responsible for injecting the WooPayConnectIframe into the @@ -94,11 +95,12 @@ class WooPayDirectCheckout { /** * Resolves the redirect URL to the WooPay checkout page or throws an error if the request fails. + * This function should only be called when we have determined the shopper is already logged in to WooPay. * * @return {string} The redirect URL. * @throws {Error} If the session data could not be sent to WooPay. */ - static async resolveWooPayRedirectUrl() { + static async getWooPayCheckoutUrl() { // We're intentionally adding a try-catch block to catch any errors // that might occur other than the known validation errors. try { @@ -121,6 +123,16 @@ class WooPayDirectCheckout { throw new Error( 'Could not retrieve WooPay checkout URL.' ); } + const { redirect_url: redirectUrl } = woopaySessionData; + if ( + ! this.validateRedirectUrl( + redirectUrl, + 'platform_checkout_key' + ) + ) { + throw new Error( 'Invalid WooPay session URL: ' + redirectUrl ); + } + return woopaySessionData.redirect_url; } catch ( error ) { throw new Error( error.message ); @@ -143,6 +155,46 @@ class WooPayDirectCheckout { ); } + /** + * Gets the necessary merchant data to create session from WooPay request or throws an error if the request fails. + * This function should only be called if we still need to determine if the shopper is logged into WooPay or not. + * + * @return {string} WooPay redirect URL with parameters. + */ + static async getWooPayMinimumSessionUrl() { + const redirectData = await this.getWooPayMinimumSesssionDataFromMerchant(); + if ( redirectData?.success === false ) { + throw new Error( + 'Could not retrieve redirect data from merchant.' + ); + } + + if ( ! this.isValidEncryptedSessionData( redirectData ) ) { + throw new Error( 'Invalid encrypted session data.' ); + } + + const testMode = getConfig( 'testMode' ); + const redirectParams = new URLSearchParams( { + checkout_redirect: 1, + blog_id: redirectData.blog_id, + session: redirectData.data.session, + iv: redirectData.data.iv, + hash: redirectData.data.hash, + testMode, + source_url: window.location.href, + } ); + + const tracksUserId = await getTracksIdentity(); + if ( tracksUserId ) { + redirectParams.append( 'tracksUserIdentity', tracksUserId ); + } + + const redirectUrl = + getConfig( 'woopayHost' ) + '/woopay/?' + redirectParams.toString(); + + return redirectUrl; + } + /** * Gets the checkout redirect elements. * @@ -183,9 +235,9 @@ class WooPayDirectCheckout { * Adds a click-event listener to the given elements that redirects to the WooPay checkout page. * * @param {*[]} elements The elements to add a click-event listener to. - * @param {boolean} useCheckoutRedirect Whether to use the `checkout_redirect` flag to let WooPay handle the checkout flow. + * @param {boolean} userIsLoggedIn True if we determined the user is already logged in, false otherwise. */ - static redirectToWooPay( elements, useCheckoutRedirect = false ) { + static redirectToWooPay( elements, userIsLoggedIn = false ) { /** * Adds a loading spinner to the given element. * @@ -258,9 +310,11 @@ class WooPayDirectCheckout { event.preventDefault(); try { - let woopayRedirectUrl = await this.resolveWooPayRedirectUrl(); - if ( useCheckoutRedirect ) { - woopayRedirectUrl += '&checkout_redirect=1'; + let woopayRedirectUrl = ''; + if ( userIsLoggedIn ) { + woopayRedirectUrl = await this.getWooPayCheckoutUrl(); + } else { + woopayRedirectUrl = await this.getWooPayMinimumSessionUrl(); } this.teardown(); @@ -291,6 +345,52 @@ class WooPayDirectCheckout { ); } + /** + * Gets the WooPay redirect data. + * + * @return {Promise|*>} Resolves to the WooPay redirect response. + */ + static async getWooPayMinimumSesssionDataFromMerchant() { + // This should always be defined, but fallback to a request in case of the unexpected. + if ( getConfig( 'woopayMinimumSessionData' ) ) { + return getConfig( 'woopayMinimumSessionData' ); + } + + return request( + buildAjaxURL( + getConfig( 'wcAjaxUrl' ), + 'get_woopay_minimum_session_data' + ), + { + _ajax_nonce: getConfig( 'woopaySessionNonce' ), + } + ); + } + + /** + * Validates a WooPay redirect URL. + * + * @param {string} redirectUrl The URL to validate. + * @param {string} requiredParam The URL parameter that is required in the URL. + * + * @return {boolean} True if URL is valid, false otherwise. + */ + static validateRedirectUrl( redirectUrl, requiredParam ) { + try { + const parsedUrl = new URL( redirectUrl ); + if ( + parsedUrl.origin !== getConfig( 'woopayHost' ) || + ! parsedUrl.searchParams.has( requiredParam ) + ) { + return false; + } + + return true; + } catch ( error ) { + return false; + } + } + /** * Prefetches the encrypted session data if not on the product page. */ diff --git a/client/components/account-status/account-tools/index.tsx b/client/components/account-status/account-tools/index.tsx index 1a7d8039829..ba5b916d123 100644 --- a/client/components/account-status/account-tools/index.tsx +++ b/client/components/account-status/account-tools/index.tsx @@ -9,7 +9,6 @@ import { addQueryArgs } from '@wordpress/url'; * Internal dependencies */ import strings from './strings'; -import { isInDevMode } from 'utils'; import './styles.scss'; import ResetAccountModal from 'wcpay/overview/modal/reset-account'; import { trackAccountReset } from 'wcpay/onboarding/tracking'; @@ -31,8 +30,6 @@ export const AccountTools: React.FC< Props > = ( props: Props ) => { const accountLink = props.accountLink; const [ modalVisible, setModalVisible ] = useState( false ); - if ( isInDevMode() ) return null; - return ( <>
diff --git a/client/components/account-status/account-tools/strings.tsx b/client/components/account-status/account-tools/strings.tsx index 47c668d0a88..752e37d8b8a 100644 --- a/client/components/account-status/account-tools/strings.tsx +++ b/client/components/account-status/account-tools/strings.tsx @@ -4,12 +4,22 @@ */ import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { isInDevMode } from 'utils'; + export default { title: __( 'Account Tools', 'woocommerce-payments' ), - description: __( - 'Payments and deposits are disabled until account setup is completed. If you are experiencing problems completing account setup, or need to change the email/country associated with your account, you can reset your account and start from the beginning.', - 'woocommerce-payments' - ), + description: isInDevMode() + ? __( + 'Your account is in sandbox mode. If you are experiencing problems completing account setup, or wish to test with a different email/country associated with your account, you can reset your account and start from the beginning.', + 'woocommerce-payments' + ) + : __( + 'Payments and deposits are disabled until account setup is completed. If you are experiencing problems completing account setup, or need to change the email/country associated with your account, you can reset your account and start from the beginning.', + 'woocommerce-payments' + ), finish: __( 'Finish setup', 'woocommerce-payments' ), reset: __( 'Reset account', 'woocommerce-payments' ), }; diff --git a/client/components/account-status/index.js b/client/components/account-status/index.js index 19fc715b1ff..9a54d660951 100755 --- a/client/components/account-status/index.js +++ b/client/components/account-status/index.js @@ -24,6 +24,7 @@ import StatusChip from './status-chip'; import './style.scss'; import './shared.scss'; import { AccountTools } from './account-tools'; +import { isInDevMode } from 'wcpay/utils'; const AccountStatusCard = ( props ) => { const { title, children, value } = props; @@ -103,7 +104,7 @@ const AccountStatusDetails = ( props ) => { } /> - { ! accountStatus.detailsSubmitted && ( + { ( ! accountStatus.detailsSubmitted || isInDevMode() ) && ( ) } { accountFees.length > 0 && ( diff --git a/client/globals.d.ts b/client/globals.d.ts index b61cabc5d5b..601ad52188f 100644 --- a/client/globals.d.ts +++ b/client/globals.d.ts @@ -45,6 +45,7 @@ declare global { minimum_manual_deposit_amounts: Record< string, number >; minimum_scheduled_deposit_amounts: Record< string, number >; }; + depositsStatus?: string; currentDeadline?: bigint; detailsSubmitted?: boolean; pastDue?: boolean; diff --git a/client/overview/modal/reset-account/strings.tsx b/client/overview/modal/reset-account/strings.tsx index 1a1c7616ff0..c097c7f850a 100644 --- a/client/overview/modal/reset-account/strings.tsx +++ b/client/overview/modal/reset-account/strings.tsx @@ -4,12 +4,22 @@ */ import { __, sprintf } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { isInDevMode } from 'utils'; + export default { title: __( 'Reset account', 'woocommerce-payments' ), - description: __( - 'If you are experiencing problems completing account setup, or need to change the email/country associated with your account, you can reset your account and start from the beginning.', - 'woocommerce-payments' - ), + description: isInDevMode() + ? __( + 'In sandbox mode, you can reset your account and onboard again at any time. Please note that all current WooPayments account details, test transactions, and deposits history will be lost.', + 'woocommerce-payments' + ) + : __( + 'If you are experiencing problems completing account setup, or need to change the email/country associated with your account, you can reset your account and start from the beginning.', + 'woocommerce-payments' + ), beforeContinue: __( 'Before you continue', 'woocommerce-payments' ), step1: sprintf( /* translators: %s: WooPayments. */ diff --git a/client/settings/advanced-settings/style.scss b/client/settings/advanced-settings/style.scss index 775e5e02b98..7af2feb30ca 100644 --- a/client/settings/advanced-settings/style.scss +++ b/client/settings/advanced-settings/style.scss @@ -9,3 +9,11 @@ margin-bottom: 1em; } } + +.settings-section__controls { + input[type='checkbox'] { + &:disabled { + cursor: not-allowed; + } + } +} diff --git a/includes/admin/class-wc-rest-woopay-session-controller.php b/includes/admin/class-wc-rest-woopay-session-controller.php new file mode 100644 index 00000000000..3dd0fd8b1f7 --- /dev/null +++ b/includes/admin/class-wc-rest-woopay-session-controller.php @@ -0,0 +1,96 @@ +namespace, + '/' . $this->rest_base, + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_session_data' ], + 'permission_callback' => [ $this, 'check_permission' ], + ] + ); + } + + /** + * Retrieve WooPay session data. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|WP_REST_Response + */ + public function get_session_data( WP_REST_Request $request ): WP_REST_Response { + // phpcs:ignore + /** + * @psalm-suppress UndefinedClass + */ + $response = WooPay_Session::get_init_session_request(); + // This was needed as the preloaded requests were not honoring the cart token and so were empty carts. + // It would be ideal to get this to successfully preload the cart data so WooPay doesn't need to make + // a separate request to get the cart data. + unset( $response['preloaded_requests'] ); + + return rest_ensure_response( $response ); + } + + /** + * Check permission confirms that the request is from WooPay. + * + * @return bool True if request is from WooPay and has a valid signature. + */ + public function check_permission() { + return $this->is_request_from_woopay() && $this->has_valid_request_signature(); + } + + /** + * Returns true if the request that's currently being processed is signed with the blog token. + * + * @return bool True if the request signature is valid. + */ + private function has_valid_request_signature() { + return apply_filters( 'wcpay_woopay_is_signed_with_blog_token', Rest_Authentication::is_signed_with_blog_token() ); + } + + /** + * Returns true if the request that's currently being processed is from WooPay, false + * otherwise. + * + * @return bool True if request is from WooPay. + */ + private function is_request_from_woopay(): bool { + return isset( $_SERVER['HTTP_USER_AGENT'] ) && 'WooPay' === $_SERVER['HTTP_USER_AGENT']; + } +} + diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 7a0e990f518..fe36e682f48 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -289,6 +289,7 @@ public function get_account_status_data(): array { 'paymentsEnabled' => $account['payments_enabled'], 'detailsSubmitted' => $account['details_submitted'] ?? true, 'deposits' => $account['deposits'] ?? [], + 'depositsStatus' => $account['deposits']['status'] ?? $account['deposits_status'] ?? '', 'currentDeadline' => $account['current_deadline'] ?? false, 'pastDue' => $account['has_overdue_requirements'] ?? false, 'accountLink' => $this->get_login_url(), @@ -1115,7 +1116,7 @@ public function maybe_handle_onboarding() { } if ( WC_Payments_Onboarding_Service::SOURCE_WCPAY_RESET_ACCOUNT === $source ) { - $test_mode = WC_Payments_Onboarding_Service::is_test_mode_enabled(); + $test_mode = WC_Payments_Onboarding_Service::is_test_mode_enabled() || WC_Payments::mode()->is_dev(); // Delete the account. $this->payments_api_client->delete_account( $test_mode ); diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index 6c4ba5ecc0d..6d28ec09e2a 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -22,7 +22,7 @@ use WC_Payment_Gateway_WCPay; use WCPay\WooPay\WooPay_Utilities; use WCPay\Payment_Methods\UPE_Payment_Method; - +use WCPay\WooPay\WooPay_Session; /** * WC_Payments_Checkout @@ -202,6 +202,7 @@ public function get_payment_fields_js_config() { 'woopaySessionNonce' => wp_create_nonce( 'woopay_session_nonce' ), 'woopayMerchantId' => Jetpack_Options::get_option( 'id' ), 'icon' => $this->gateway->get_icon_url(), + 'woopayMinimumSessionData' => WooPay_Session::get_woopay_minimum_session_data(), ]; /** diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 524e917b0ab..c53436b4aad 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -1054,6 +1054,10 @@ public static function init_rest_api() { $reports_authorizations_controller = new WC_REST_Payments_Reports_Authorizations_Controller( self::$api_client ); $reports_authorizations_controller->register_routes(); + include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-woopay-session-controller.php'; + $woopay_session_controller = new WC_REST_WooPay_Session_Controller(); + $woopay_session_controller->register_routes(); + } /** @@ -1426,6 +1430,7 @@ public static function maybe_register_woopay_hooks() { add_action( 'wc_ajax_wcpay_init_woopay', [ WooPay_Session::class, 'ajax_init_woopay' ] ); add_action( 'wc_ajax_wcpay_get_woopay_session', [ WooPay_Session::class, 'ajax_get_woopay_session' ] ); add_action( 'wc_ajax_wcpay_get_woopay_signature', [ __CLASS__, 'ajax_get_woopay_signature' ] ); + add_action( 'wc_ajax_wcpay_get_woopay_minimum_session_data', [ WooPay_Session::class, 'ajax_get_woopay_minimum_session_data' ] ); // This injects the payments API and draft orders into core, so the WooCommerce Blocks plugin is not necessary. // We should remove this once both features are available by default in the WC minimum supported version. diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php index f40a9d24fcc..839b4d943c3 100644 --- a/includes/woopay/class-woopay-session.php +++ b/includes/woopay/class-woopay-session.php @@ -334,31 +334,7 @@ public static function get_frontend_init_session_request() { $session = self::get_init_session_request( $order_id, $key, $billing_email ); - $store_blog_token = ( WooPay_Utilities::get_woopay_url() === WooPay_Utilities::DEFAULT_WOOPAY_URL ) ? Jetpack_Options::get_option( 'blog_token' ) : 'dev_mode'; - - $message = wp_json_encode( $session ); - - // Generate an initialization vector (IV) for encryption. - $iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( 'aes-256-cbc' ) ); - - // Encrypt the JSON session. - $session_encrypted = openssl_encrypt( $message, 'aes-256-cbc', $store_blog_token, OPENSSL_RAW_DATA, $iv ); - - // Create an HMAC hash for data integrity. - $hash = hash_hmac( 'sha256', $session_encrypted, $store_blog_token ); - - $data = [ - 'session' => $session_encrypted, - 'iv' => $iv, - 'hash' => $hash, - ]; - - $response = [ - 'blog_id' => Jetpack_Options::get_option( 'id' ), - 'data' => array_map( 'base64_encode', $data ), - ]; - - return $response; + return WooPay_Utilities::encrypt_and_sign_data( $session ); } /** @@ -369,7 +345,7 @@ public static function get_frontend_init_session_request() { * @param string|null $billing_email Pay-for-order billing email. * @return array The initial session request data without email and user_session. */ - private static function get_init_session_request( $order_id = null, $key = null, $billing_email = null ) { + public static function get_init_session_request( $order_id = null, $key = null, $billing_email = null ) { $user = wp_get_current_user(); $is_pay_for_order = null !== $order_id; $order = wc_get_order( $order_id ); @@ -556,6 +532,58 @@ public static function ajax_get_woopay_session() { wp_send_json( self::get_frontend_init_session_request() ); } + /** + * Used to initialize woopay session on frontend + * + * @return void + */ + public static function ajax_get_woopay_minimum_session_data() { + $is_nonce_valid = check_ajax_referer( 'woopay_session_nonce', false, false ); + + if ( ! $is_nonce_valid ) { + wp_send_json_error( + __( 'You aren’t authorized to do that.', 'woocommerce-payments' ), + 403 + ); + } + + $blog_id = Jetpack_Options::get_option('id'); + if ( empty( $blog_id ) ) { + wp_send_json_error( + __( 'Could not determine the blog ID.', 'woocommerce-payments' ), + 503 + ); + } + + wp_send_json( self::get_woopay_minimum_session_data() ); + } + + /** + * Return WooPay minimum session data. + * + * @return array Array of minimum session data used by WooPay or false on failures. + */ + public static function get_woopay_minimum_session_data() { + if ( ! WC_Payments_Features::is_client_secret_encryption_eligible() ) { + return []; + } + + $blog_id = Jetpack_Options::get_option('id'); + if ( empty( $blog_id ) ) { + return []; + } + + $data = [ + 'blog_id' => $blog_id, + 'blog_rest_url' => get_rest_url(), + 'blog_checkout_url' => wc_get_checkout_url(), + 'session_nonce' => self::create_woopay_nonce( get_current_user_id() ), + 'store_api_token' => self::init_store_api_token(), + ]; + + return WooPay_Utilities::encrypt_and_sign_data( $data ); + } + /** * Get the WooPay verified email address from the header. * diff --git a/includes/woopay/class-woopay-utilities.php b/includes/woopay/class-woopay-utilities.php index dc13d5c9d46..9b3fdacd66e 100644 --- a/includes/woopay/class-woopay-utilities.php +++ b/includes/woopay/class-woopay-utilities.php @@ -12,6 +12,7 @@ use WooPay_Extension; use WC_Geolocation; use WC_Payments; +use Jetpack_Options; /** * WooPay @@ -262,6 +263,38 @@ public function has_adapted_extension_installed() { return false; } + /** + * Return an array with encrypted and signed data. + * + * @param array $data The data to be encrypted and signed. + * @return array The encrypted and signed data. + */ + public static function encrypt_and_sign_data( $data ) { + $store_blog_token = ( self::get_woopay_url() === self::DEFAULT_WOOPAY_URL ) ? Jetpack_Options::get_option( 'blog_token' ) : 'dev_mode'; + + $message = wp_json_encode( $data ); + + // Generate an initialization vector (IV) for encryption. + $iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( 'aes-256-cbc' ) ); + + // Encrypt the JSON session. + $session_encrypted = openssl_encrypt( $message, 'aes-256-cbc', $store_blog_token, OPENSSL_RAW_DATA, $iv ); + + // Create an HMAC hash for data integrity. + $hash = hash_hmac( 'sha256', $session_encrypted, $store_blog_token ); + + $data = [ + 'session' => $session_encrypted, + 'iv' => $iv, + 'hash' => $hash, + ]; + + return [ + 'blog_id' => Jetpack_Options::get_option( 'id' ), + 'data' => array_map( 'base64_encode', $data ), + ]; + } + /** * Get the persisted available countries. *