From cb65e92cc9d3a263713a22998439e1cfad23d6ec Mon Sep 17 00:00:00 2001 From: Cvetan Cvetanov Date: Sun, 15 Dec 2024 21:15:46 +0200 Subject: [PATCH] Add support for utilizing NOX capabilities as URL parameters during account creation (#9947) Co-authored-by: oaratovskyi Co-authored-by: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com> Co-authored-by: Vlad Olaru --- ...s-capabilities-to-onboarding-as-get-params | 4 + client/connect-account-page/index.tsx | 1 + client/onboarding/utils.ts | 2 + includes/class-wc-payments-account.php | 44 +++++-- .../class-wc-payments-onboarding-service.php | 108 +++++++++++++++++- 5 files changed, 144 insertions(+), 15 deletions(-) create mode 100644 changelog/update-pass-capabilities-to-onboarding-as-get-params diff --git a/changelog/update-pass-capabilities-to-onboarding-as-get-params b/changelog/update-pass-capabilities-to-onboarding-as-get-params new file mode 100644 index 00000000000..9104e7a8f99 --- /dev/null +++ b/changelog/update-pass-capabilities-to-onboarding-as-get-params @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add support for utilizing NOX capabilities as URL parameters during account creation. diff --git a/client/connect-account-page/index.tsx b/client/connect-account-page/index.tsx index 6502975d642..2b3f402abcb 100644 --- a/client/connect-account-page/index.tsx +++ b/client/connect-account-page/index.tsx @@ -215,6 +215,7 @@ const ConnectAccountPage: React.FC = () => { const customizedConnectUrl = addQueryArgs( connectUrl, { test_drive: 'true', + capabilities: urlParams.get( 'capabilities' ) || '', } ); const updateProgress = setInterval( updateLoaderProgress, 2500, 40, 5 ); diff --git a/client/onboarding/utils.ts b/client/onboarding/utils.ts index a95c3e298ef..306328f64f7 100644 --- a/client/onboarding/utils.ts +++ b/client/onboarding/utils.ts @@ -65,9 +65,11 @@ export const createAccountSession = async ( data: OnboardingFields, isPoEligible: boolean ): Promise< AccountKycSession > => { + const urlParams = new URLSearchParams( window.location.search ); return await apiFetch< AccountKycSession >( { path: addQueryArgs( `${ NAMESPACE }/onboarding/kyc/session`, { self_assessment: fromDotNotation( data ), + capabilities: urlParams.get( 'capabilities' ) || '', progressive: isPoEligible, } ), method: 'GET', diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 097ef468710..dc5430d6f0f 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -1497,7 +1497,7 @@ public function maybe_handle_onboarding() { // If there is a working one, we can proceed with the Stripe account handling. try { $this->maybe_init_jetpack_connection( - // Carry over all the important GET params, so we have them after the Jetpack connection setup. + // Carry over all the important GET params, so we have them after the Jetpack connection setup. add_query_arg( [ 'promo' => ! empty( $incentive_id ) ? $incentive_id : false, @@ -1506,6 +1506,10 @@ public function maybe_handle_onboarding() { 'test_mode' => $should_onboard_in_test_mode ? 'true' : false, 'test_drive' => $create_test_drive_account ? 'true' : false, 'auto_start_test_drive_onboarding' => $auto_start_test_drive_onboarding ? 'true' : false, + // These are starting capabilities for the account. + // They are collected by the payment method step of the + // WC Payments settings page native onboarding experience. + 'capabilities' => rawurlencode( wp_json_encode( $this->onboarding_service->get_capabilities_from_request() ) ), 'from' => WC_Payments_Onboarding_Service::FROM_WPCOM_CONNECTION, 'source' => $onboarding_source, 'redirect_to_settings_page' => $redirect_to_settings_page ? 'true' : false, @@ -1534,13 +1538,19 @@ public function maybe_handle_onboarding() { && WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD !== $from && ! $this->is_stripe_connected() ) { + $additional_params = [ + 'source' => $onboarding_source, + ]; + + if ( $this->onboarding_service->get_capabilities_from_request() ) { + $additional_params['capabilities'] = rawurlencode( wp_json_encode( $this->onboarding_service->get_capabilities_from_request() ) ); + } + $this->redirect_service->redirect_to_onboarding_wizard( // When we redirect to the onboarding wizard, we carry over the `from`, if we have it. // This is because there is no interim step between the user clicking the connect link and the onboarding wizard. ! empty( $from ) ? $from : $next_step_from, - [ - 'source' => $onboarding_source, - ] + $additional_params ); return; } @@ -1573,11 +1583,15 @@ public function maybe_handle_onboarding() { null, $from, // Carry over `from` since we are doing a short-circuit. [ - 'promo' => ! empty( $incentive_id ) ? $incentive_id : false, - 'test_drive' => 'true', + 'promo' => ! empty( $incentive_id ) ? $incentive_id : false, + 'test_drive' => 'true', 'auto_start_test_drive_onboarding' => 'true', // This is critical. - 'test_mode' => $should_onboard_in_test_mode ? 'true' : false, - 'source' => $onboarding_source, + // These are starting capabilities for the account. + // They are collected by the payment method step of the + // WC Payments settings page native onboarding experience. + 'capabilities' => rawurlencode( wp_json_encode( $this->onboarding_service->get_capabilities_from_request() ) ), + 'test_mode' => $should_onboard_in_test_mode ? 'true' : false, + 'source' => $onboarding_source, 'redirect_to_settings_page' => $redirect_to_settings_page ? 'true' : false, ] ); @@ -1982,6 +1996,7 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne } $self_assessment_data = isset( $_GET['self_assessment'] ) ? wc_clean( wp_unslash( $_GET['self_assessment'] ) ) : []; + if ( 'test_drive' === $setup_mode ) { // If we get to the overview page, we want to show the success message. $return_url = add_query_arg( 'wcpay-sandbox-success', 'true', $return_url ); @@ -1996,7 +2011,14 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne ]; $user_data = $this->onboarding_service->get_onboarding_user_data(); - $account_data = $this->onboarding_service->get_account_data( $setup_mode, $self_assessment_data ); + $account_data = $this->onboarding_service->get_account_data( + $setup_mode, + $self_assessment_data, + // These are starting capabilities for the account. + // They are collected by the payment method step of the + // WC Payments settings page native onboarding experience. + $this->onboarding_service->get_capabilities_from_request() + ); $onboarding_data = $this->payments_api_client->get_onboarding_data( 'live' === $setup_mode, @@ -2594,7 +2616,9 @@ public function get_lifetime_total_payment_volume(): int { } /** - * Extract the test drive settings from the account data that we want to store for the live account. + * Extract the useful test drive settings from the account data. + * + * We will use this data to migrate the test drive settings when onboarding the live account. * ATM we only store the enabled payment methods. * * @return array The test drive settings for the live account. diff --git a/includes/class-wc-payments-onboarding-service.php b/includes/class-wc-payments-onboarding-service.php index ce558ac2faf..504ac3bd5e4 100644 --- a/includes/class-wc-payments-onboarding-service.php +++ b/includes/class-wc-payments-onboarding-service.php @@ -180,6 +180,69 @@ function () use ( $country_code, $locale ) { ); } + /** + * Get the onboarding capabilities from the request. + * + * The capabilities are expected to be passed as an array of capabilities keyed by the capability ID and + * with boolean values. If the value is true, the capability is requested when the account is created. + * + * @return array The standardized capabilities that were passed in the request. + * Empty array if no capabilities were passed or none were valid. + */ + public function get_capabilities_from_request(): array { + $capabilities = []; + + if ( empty( $_REQUEST['capabilities'] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended + return $capabilities; + } + + // Try to extract the capabilities. + // They might be already decoded or not, so we need to handle both cases. + // We expect them to be an array. + // We disable the warning because we have our own sanitization and validation. + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $capabilities = wp_unslash( $_REQUEST['capabilities'] ); + if ( ! is_array( $capabilities ) ) { + $capabilities = json_decode( $capabilities, true ) ?? []; + } + + if ( empty( $capabilities ) ) { + return []; + } + + // Sanitize and validate. + $capabilities = array_combine( + array_map( + function ( $key ) { + // Keep numeric keys as integers so we can remove them later. + if ( is_numeric( $key ) ) { + return intval( $key ); + } + + return sanitize_text_field( $key ); + }, + array_keys( $capabilities ) + ), + array_map( + function ( $value ) { + return filter_var( $value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); + }, + $capabilities + ) + ); + + // Filter out any invalid entries. + $capabilities = array_filter( + $capabilities, + function ( $value, $key ) { + return is_string( $key ) && is_bool( $value ); + }, + ARRAY_FILTER_USE_BOTH + ); + + return $capabilities; + } + /** * Retrieve the embedded KYC session and handle initial account creation (if necessary). * @@ -207,15 +270,19 @@ public function create_embedded_kyc_session( array $self_assessment_data, bool $ 'site_locale' => get_locale(), ]; $user_data = $this->get_onboarding_user_data(); - $account_data = $this->get_account_data( $setup_mode, $self_assessment_data ); + $account_data = $this->get_account_data( + $setup_mode, + $self_assessment_data, + $this->get_capabilities_from_request() + ); $actioned_notes = self::get_actioned_notes(); try { $account_session = $this->payments_api_client->initialize_onboarding_embedded_kyc( 'live' === $setup_mode, $site_data, - array_filter( $user_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. - array_filter( $account_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. + WC_Payments_Utils::array_filter_recursive( $user_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. + WC_Payments_Utils::array_filter_recursive( $account_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. $actioned_notes, $progressive ); @@ -365,12 +432,15 @@ public function add_admin_body_classes( string $classes = '' ): string { /** * Get account data for onboarding from self assessment data. * - * @param string $setup_mode Setup mode. + * @param string $setup_mode Setup mode. * @param array $self_assessment_data Self assessment data. + * @param array $capabilities Optional. List keyed by capabilities IDs (payment methods) with boolean values. + * If the value is true, the capability is requested when the account is created. + * If the value is false, the capability is not requested when the account is created. * * @return array Account data. */ - public function get_account_data( string $setup_mode, array $self_assessment_data ): array { + public function get_account_data( string $setup_mode, array $self_assessment_data, array $capabilities = [] ): array { $home_url = get_home_url(); // If the site is running on localhost, use a bogus URL. This is to avoid Stripe's errors. // wp_http_validate_url does not check that, unfortunately. @@ -387,6 +457,33 @@ public function get_account_data( string $setup_mode, array $self_assessment_dat 'business_name' => get_bloginfo( 'name' ), ]; + foreach ( $capabilities as $capability => $should_request ) { + // Remove the `_payments` suffix from the capability, if present. + if ( strpos( $capability, '_payments' ) === strlen( $capability ) - 9 ) { + $capability = str_replace( '_payments', '', $capability ); + } + + // Skip the special 'apple_google' because it is not a payment method. + // Skip the 'woopay' because it is automatically handled by the API. + if ( 'apple_google' === $capability || 'woopay' === $capability ) { + continue; + } + + if ( 'card' === $capability ) { + // Card is always requested. + $account_data['capabilities']['card_payments'] = [ 'requested' => 'true' ]; + // When requesting card, we also need to request transfers. + // The platform should handle this automatically, but it is best to be thorough. + $account_data['capabilities']['transfers'] = [ 'requested' => 'true' ]; + continue; + } + + // We only request, not unrequest capabilities. + if ( $should_request ) { + $account_data['capabilities'][ $capability . '_payments' ] = [ 'requested' => 'true' ]; + } + } + if ( ! empty( $self_assessment_data ) ) { $business_type = $self_assessment_data['business_type'] ?? null; $account_data = WC_Payments_Utils::array_merge_recursive_distinct( @@ -436,6 +533,7 @@ public function get_account_data( string $setup_mode, array $self_assessment_dat ] ); } + return $account_data; }