diff --git a/changelog/dev-include-connect-js b/changelog/dev-include-connect-js
new file mode 100644
index 00000000000..218ec497755
--- /dev/null
+++ b/changelog/dev-include-connect-js
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Added Embdedded KYC, currently behind feature flag.
diff --git a/client/globals.d.ts b/client/globals.d.ts
index f00b521943a..44d94baa01b 100644
--- a/client/globals.d.ts
+++ b/client/globals.d.ts
@@ -16,6 +16,7 @@ declare global {
paymentTimeline: boolean;
isDisputeIssuerEvidenceEnabled: boolean;
isPaymentOverviewWidgetEnabled?: boolean;
+ isEmbeddedKycEnabled?: boolean;
};
fraudServices: unknown[];
testMode: boolean;
diff --git a/client/index.js b/client/index.js
index 9a530a54010..a5eb2d8376c 100644
--- a/client/index.js
+++ b/client/index.js
@@ -29,6 +29,7 @@ import CapitalPage from 'capital';
import OverviewPage from 'overview';
import DocumentsPage from 'documents';
import OnboardingPage from 'onboarding';
+import OnboardingKycPage from 'onboarding/kyc';
import FraudProtectionAdvancedSettingsPage from './settings/fraud-protection/advanced-settings';
import { getTasks } from 'overview/task-list/tasks';
@@ -69,6 +70,26 @@ addFilter(
capability: 'manage_woocommerce',
} );
+ // Currently under feature flag.
+ if (
+ wcpaySettings &&
+ wcpaySettings.featureFlags.isEmbeddedKycEnabled
+ ) {
+ pages.push( {
+ container: OnboardingKycPage,
+ path: '/payments/onboarding/kyc',
+ wpOpenMenu: menuID,
+ breadcrumbs: [
+ rootLink,
+ __( 'Continue onboarding', 'woocommerce-payments' ),
+ ],
+ navArgs: {
+ id: 'wc-payments-continue-onboarding',
+ },
+ capability: 'manage_woocommerce',
+ } );
+ }
+
pages.push( {
container: OverviewPage,
path: '/payments/overview',
diff --git a/client/onboarding/index.tsx b/client/onboarding/index.tsx
index 5def61ebd4b..62e179bb7c2 100644
--- a/client/onboarding/index.tsx
+++ b/client/onboarding/index.tsx
@@ -13,11 +13,12 @@ import { getMccFromIndustry } from 'onboarding/utils';
import { OnboardingForm } from './form';
import Step from './step';
import BusinessDetails from './steps/business-details';
+import EmbeddedKyc from './steps/embedded-kyc';
import StoreDetails from './steps/store-details';
-import LoadingStep from './steps/loading';
import { trackStarted } from './tracking';
import { getAdminUrl } from 'wcpay/utils';
import './style.scss';
+import LoadingStep from 'wcpay/onboarding/steps/loading';
const OnboardingStepper = () => {
const handleExit = () => {
@@ -47,7 +48,13 @@ const OnboardingStepper = () => {
-
+ { wcpaySettings?.featureFlags?.isEmbeddedKycEnabled ? (
+
+
+
+ ) : (
+
+ ) }
);
};
diff --git a/client/onboarding/kyc/appearance.ts b/client/onboarding/kyc/appearance.ts
new file mode 100644
index 00000000000..db0260c302f
--- /dev/null
+++ b/client/onboarding/kyc/appearance.ts
@@ -0,0 +1,62 @@
+/* eslint-disable max-len */
+
+/**
+ * Customised appearance variables for the external KYC flow.
+ */
+export default {
+ variables: {
+ colorPrimary: '#3C2861',
+ colorBackground: '#FFFFFF',
+ buttonPrimaryColorBackground: '#3858E9',
+ buttonPrimaryColorBorder: '#3858E9',
+ buttonPrimaryColorText: '#FFFFFF',
+ buttonSecondaryColorBackground: '#FFFFFF',
+ buttonSecondaryColorBorder: '#3858E9',
+ buttonSecondaryColorText: '#3858E9',
+ colorText: '#101517',
+ colorSecondaryText: '#50575E',
+ actionPrimaryColorText: '#3858E9',
+ actionSecondaryColorText: '#101517',
+ colorBorder: '#DDDDDD',
+ formHighlightColorBorder: '#3858E9',
+ formAccentColor: '#3858E9',
+ colorDanger: '#CC1818',
+ offsetBackgroundColor: '#F0F0F0',
+ formBackgroundColor: '#FFFFFF',
+ badgeNeutralColorText: '#2C3338',
+ badgeNeutralColorBackground: '#F6F7F7',
+ badgeNeutralColorBorder: '#F6F7F7',
+ badgeSuccessColorText: '#005C12',
+ badgeSuccessColorBackground: '#EDFAEF',
+ badgeSuccessColorBorder: '#EDFAEF',
+ badgeWarningColorText: '#614200',
+ badgeWarningColorBackground: '#FCF9E8',
+ badgeWarningColorBorder: '#FCF9E8',
+ badgeDangerColorText: '#8A2424',
+ badgeDangerColorBackground: '#FCF0F1',
+ badgeDangerColorBorder: '#FCF0F1',
+ borderRadius: '2px',
+ buttonBorderRadius: '2px',
+ formBorderRadius: '2px',
+ badgeBorderRadius: '2px',
+ overlayBorderRadius: '8px',
+ spacingUnit: '10px',
+ fontFamily:
+ "-apple-system, BlinkMacSystemFont, 'system-ui', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Roboto', 'Arial', sans-serif",
+ fontSizeBase: '16px',
+ headingXlFontSize: '32px',
+ headingXlFontWeight: '400',
+ headingLgFontSize: '24px',
+ headingLgFontWeight: '400',
+ headingMdFontSize: '20px',
+ headingMdFontWeight: '400',
+ headingSmFontSize: '16px',
+ headingSmFontWeight: '600',
+ headingXsFontSize: '12px',
+ headingXsFontWeight: '600',
+ bodyMdFontWeight: '400',
+ bodyMdFontSize: '16px',
+ bodySmFontSize: '14px',
+ bodySmFontWeight: '400',
+ },
+};
diff --git a/client/onboarding/kyc/index.tsx b/client/onboarding/kyc/index.tsx
new file mode 100644
index 00000000000..d2e9a4b5140
--- /dev/null
+++ b/client/onboarding/kyc/index.tsx
@@ -0,0 +1,77 @@
+/**
+ * External dependencies
+ */
+import React, { useEffect } from 'react';
+import { closeSmall, Icon } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import Logo from 'assets/images/woopayments.svg';
+import Page from 'components/page';
+import { OnboardingContextProvider } from 'onboarding/context';
+import EmbeddedKyc from 'onboarding/steps/embedded-kyc';
+import strings from 'onboarding/strings';
+import { getConnectUrl } from 'utils';
+
+const OnboardingKycPage: React.FC = () => {
+ const handleExit = () => {
+ const urlParams = new URLSearchParams( window.location.search );
+
+ window.location.href = getConnectUrl(
+ {
+ source:
+ urlParams.get( 'source' )?.replace( /[^\w-]+/g, '' ) ||
+ 'unknown',
+ },
+ 'WCPAY_ONBOARDING_KYC'
+ );
+ };
+
+ useEffect( () => {
+ // Remove loading class and add those required for full screen.
+ document.body.classList.remove( 'woocommerce-admin-is-loading' );
+ document.body.classList.add( 'woocommerce-admin-full-screen' );
+ document.body.classList.add( 'is-wp-toolbar-disabled' );
+ document.body.classList.add( 'wcpay-onboarding__body' );
+
+ // Remove full screen classes on unmount.
+ return () => {
+ document.body.classList.remove( 'woocommerce-admin-full-screen' );
+ document.body.classList.remove( 'is-wp-toolbar-disabled' );
+ document.body.classList.remove( 'wcpay-onboarding__body' );
+ };
+ }, [] );
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+export default OnboardingKycPage;
diff --git a/client/onboarding/step.tsx b/client/onboarding/step.tsx
index c6f4aee0b17..92ed2e4e3d4 100644
--- a/client/onboarding/step.tsx
+++ b/client/onboarding/step.tsx
@@ -17,9 +17,10 @@ import './style.scss';
interface Props {
name: OnboardingSteps;
+ showHeading?: boolean;
}
-const Step: React.FC< Props > = ( { name, children } ) => {
+const Step: React.FC< Props > = ( { name, children, showHeading = true } ) => {
const { trackAbandoned } = useTrackAbandoned();
const { prevStep, exit } = useStepperContext();
const handleExit = () => {
@@ -36,7 +37,9 @@ const Step: React.FC< Props > = ( { name, children } ) => {
-
- { strings.steps[ name ].heading }
-
-
- { strings.steps[ name ].subheading }
-
+ { showHeading && (
+ <>
+
+ { strings.steps[ name ].heading }
+
+
+ { strings.steps[ name ].subheading }
+
+ >
+ ) }
{ children }
>
diff --git a/client/onboarding/steps/embedded-kyc.tsx b/client/onboarding/steps/embedded-kyc.tsx
new file mode 100644
index 00000000000..1ca2c9c6a92
--- /dev/null
+++ b/client/onboarding/steps/embedded-kyc.tsx
@@ -0,0 +1,234 @@
+/**
+ * External dependencies
+ */
+import React, { useEffect, useState } from 'react';
+import {
+ loadConnectAndInitialize,
+ StripeConnectInstance,
+} from '@stripe/connect-js';
+import { LoadError } from '@stripe/connect-js/types/config';
+import {
+ ConnectAccountOnboarding,
+ ConnectComponentsProvider,
+} from '@stripe/react-connect-js';
+import apiFetch from '@wordpress/api-fetch';
+import { __ } from '@wordpress/i18n';
+import { addQueryArgs } from '@wordpress/url';
+
+/**
+ * Internal dependencies
+ */
+import { NAMESPACE } from 'data/constants';
+import appearance from '../kyc/appearance';
+import BannerNotice from 'wcpay/components/banner-notice';
+import LoadBar from 'wcpay/components/load-bar';
+import { useOnboardingContext } from 'wcpay/onboarding/context';
+import {
+ AccountKycSession,
+ PoEligibleData,
+ PoEligibleResult,
+} from 'wcpay/onboarding/types';
+import { fromDotNotation } from 'wcpay/onboarding/utils';
+import { getConnectUrl, getOverviewUrl } from 'wcpay/utils';
+
+type AccountKycSessionData = AccountKycSession;
+
+interface FinalizeResponse {
+ success: boolean;
+ params: Record< string, string >;
+}
+
+interface Props {
+ continueKyc?: boolean;
+}
+
+const EmbeddedKyc: React.FC< Props > = ( { continueKyc = false } ) => {
+ const { data } = useOnboardingContext();
+ const [ publishableKey, setPublishableKey ] = useState( '' );
+ const [ locale, setLocale ] = useState( '' );
+ const [ clientSecret, setClientSecret ] = useState<
+ ( () => Promise< string > ) | null
+ >( null );
+ const [
+ stripeConnectInstance,
+ setStripeConnectInstance,
+ ] = useState< StripeConnectInstance | null >( null );
+ const [ loading, setLoading ] = useState( true );
+ const [ loadErrorMessage, setLoadErrorMessage ] = useState( '' );
+ const onLoaderStart = () => {
+ setLoading( false );
+ };
+ const onLoadError = ( loadError: LoadError ) => {
+ setLoadErrorMessage( loadError.error.message || 'Unknown error' );
+ };
+
+ useEffect( () => {
+ const isEligibleForPo = async () => {
+ if (
+ ! data.country ||
+ ! data.business_type ||
+ ! data.mcc ||
+ ! data.annual_revenue ||
+ ! data.go_live_timeframe
+ ) {
+ return false;
+ }
+ const eligibilityDetails: PoEligibleData = {
+ business: {
+ country: data.country,
+ type: data.business_type,
+ mcc: data.mcc,
+ },
+ store: {
+ annual_revenue: data.annual_revenue,
+ go_live_timeframe: data.go_live_timeframe,
+ },
+ };
+
+ try {
+ const eligibleResult = await apiFetch< PoEligibleResult >( {
+ path: '/wc/v3/payments/onboarding/router/po_eligible',
+ method: 'POST',
+ data: eligibilityDetails,
+ } );
+
+ return 'eligible' === eligibleResult.result;
+ } catch ( error ) {
+ // Fall back to full KYC scenario.
+ return false;
+ }
+ };
+
+ const fetchKeys = async () => {
+ // By default, we assume the merchant is not eligible for PO.
+ let isEligible = false;
+
+ // If we are resuming an onboarding session, we don't need to check for PO eligibility again.
+ if ( ! continueKyc ) {
+ isEligible = await isEligibleForPo();
+ }
+
+ const path = addQueryArgs(
+ `${ NAMESPACE }/onboarding/kyc/session`,
+ {
+ self_assessment: fromDotNotation( data ),
+ progressive: isEligible,
+ }
+ );
+ const accountSession = await apiFetch< AccountKycSessionData >( {
+ path: path,
+ method: 'GET',
+ } );
+ if (
+ accountSession.publishableKey &&
+ accountSession.clientSecret
+ ) {
+ setPublishableKey( accountSession.publishableKey );
+ setLocale( accountSession.locale );
+ setClientSecret( () => () =>
+ Promise.resolve( accountSession.clientSecret )
+ ); // Ensure clientSecret is wrapped as a function returning a Promise
+ } else {
+ setLoading( false );
+ setLoadErrorMessage(
+ __(
+ "Failed to create account session. Please check that you're using the latest version of WooCommerce Payments.",
+ 'woocommerce-payments'
+ )
+ );
+ }
+ };
+
+ fetchKeys();
+ }, [ data, continueKyc ] );
+
+ // Initialize the Stripe Connect instance only once when publishableKey and clientSecret are ready
+ useEffect( () => {
+ if ( publishableKey && clientSecret && ! stripeConnectInstance ) {
+ const stripeInstance = loadConnectAndInitialize( {
+ publishableKey: publishableKey,
+ fetchClientSecret: clientSecret, // Pass the function returning the Promise
+ appearance: {
+ // See all possible variables below
+ overlays: 'drawer',
+ variables: appearance.variables,
+ },
+ locale: locale.replace( '_', '-' ),
+ } );
+
+ setStripeConnectInstance( stripeInstance );
+ }
+ }, [ publishableKey, clientSecret, stripeConnectInstance, locale ] );
+
+ return (
+ <>
+ { loading && }
+ { loadErrorMessage && (
+ { loadErrorMessage }
+ ) }
+ { stripeConnectInstance && (
+
+ {
+ const urlParams = new URLSearchParams(
+ window.location.search
+ );
+ const urlSource =
+ urlParams
+ .get( 'source' )
+ ?.replace( /[^\w-]+/g, '' ) || 'unknown';
+ try {
+ const response = await apiFetch<
+ FinalizeResponse
+ >( {
+ path: `${ NAMESPACE }/onboarding/kyc/finalize`,
+ method: 'POST',
+ data: {
+ source: urlSource,
+ from: 'WCPAY_ONBOARDING_WIZARD',
+ clientSecret: clientSecret,
+ },
+ } );
+
+ if ( response.success ) {
+ window.location.href = getOverviewUrl(
+ {
+ ...response.params,
+ 'wcpay-connection-success': '1',
+ },
+ 'WCPAY_ONBOARDING_WIZARD'
+ );
+ } else {
+ // If a non-success response is received we should redirect to the Connect page with an error flag:
+ window.location.href = getConnectUrl(
+ {
+ ...response.params,
+ 'wcpay-connection-error': '1',
+ },
+ 'WCPAY_ONBOARDING_WIZARD'
+ );
+ }
+ } catch ( error ) {
+ // If an error response is received we should redirect to the Connect page with an error flag:
+ // Note that this should never happen, since we always expect a response from the server.
+ window.location.href = getConnectUrl(
+ {
+ 'wcpay-connection-error': '1',
+ source: urlSource,
+ },
+ 'WCPAY_ONBOARDING_WIZARD'
+ );
+ }
+ } }
+ />
+
+ ) }
+ >
+ );
+};
+
+export default EmbeddedKyc;
diff --git a/client/onboarding/steps/test/embedded-onboarding.tsx b/client/onboarding/steps/test/embedded-onboarding.tsx
new file mode 100644
index 00000000000..bd3ee34b1bd
--- /dev/null
+++ b/client/onboarding/steps/test/embedded-onboarding.tsx
@@ -0,0 +1,93 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import apiFetch from '@wordpress/api-fetch';
+import { loadConnectAndInitialize } from '@stripe/connect-js';
+
+/**
+ * Internal dependencies
+ */
+import EmbeddedKyc from '../embedded-kyc';
+
+jest.mock( '@wordpress/api-fetch' );
+jest.mock( '@stripe/connect-js', () => ( {
+ loadConnectAndInitialize: jest.fn(),
+} ) );
+
+// Mock data, setData from OnboardingContext
+const data = {
+ country: 'US',
+ business_type: 'individual',
+ mcc: 'most_popular__software_services',
+ annual_revenue: 'less_than_250k',
+ go_live_timeframe: 'within_1month',
+};
+const setData = jest.fn();
+
+jest.mock( '../../context', () => ( {
+ useOnboardingContext: jest.fn( () => ( {
+ data,
+ setData,
+ } ) ),
+} ) );
+
+jest.mock( 'components/stepper', () => ( {
+ useStepperContext: jest.fn( () => ( {
+ currentStep: 'loading',
+ } ) ),
+} ) );
+
+describe( 'EmbeddedOnboarding', () => {
+ beforeEach( () => {
+ jest.clearAllMocks(); // Clear all mocks between tests
+ } );
+
+ it( 'should show error if account session fails', async () => {
+ jest.mocked( apiFetch ).mockResolvedValueOnce( {
+ result: 'eligible',
+ data: [],
+ } );
+
+ jest.mocked( apiFetch ).mockResolvedValueOnce( {
+ success: false,
+ } );
+
+ render( );
+
+ await waitFor( () =>
+ expect(
+ screen.getByText( /Failed to create account session/i )
+ ).toBeInTheDocument()
+ );
+ } );
+
+ it( 'should initialize Stripe Connect when publishableKey and clientSecret are set', async () => {
+ jest.mocked( apiFetch ).mockResolvedValueOnce( {
+ result: 'eligible',
+ data: [],
+ } );
+
+ const mockAccountSessionData = {
+ publishableKey: 'test_publishable_key',
+ clientSecret: 'test_client_secret',
+ locale: 'en_US',
+ };
+ jest.mocked( apiFetch ).mockResolvedValueOnce( mockAccountSessionData );
+
+ render( );
+
+ await waitFor( () =>
+ expect( loadConnectAndInitialize ).toHaveBeenCalledWith( {
+ publishableKey: mockAccountSessionData.publishableKey,
+ fetchClientSecret: expect.any( Function ),
+ appearance: {
+ overlays: 'drawer',
+ variables: expect.any( Object ),
+ },
+ locale: 'en-US', // Locale should be formatted correctly
+ } )
+ );
+ } );
+} );
diff --git a/client/onboarding/strings.tsx b/client/onboarding/strings.tsx
index 02ea32d2998..432e4aae21d 100644
--- a/client/onboarding/strings.tsx
+++ b/client/onboarding/strings.tsx
@@ -44,6 +44,16 @@ export default {
'woocommerce-payments'
),
},
+ embedded: {
+ heading: __(
+ 'One last step! Verify your identity with our partner',
+ 'woocommerce-payments'
+ ),
+ subheading: __(
+ 'This info will verify your account',
+ 'woocommerce-payments'
+ ),
+ },
},
fields: {
country: __(
diff --git a/client/onboarding/style.scss b/client/onboarding/style.scss
index 7a5efb46b0c..eb0c84771f6 100644
--- a/client/onboarding/style.scss
+++ b/client/onboarding/style.scss
@@ -36,6 +36,10 @@ body.wcpay-onboarding__body {
&:last-child {
justify-self: end;
}
+
+ &.hide {
+ visibility: hidden;
+ }
}
&-logo {
diff --git a/client/onboarding/types.ts b/client/onboarding/types.ts
index 2f57aa08e94..c2781390524 100644
--- a/client/onboarding/types.ts
+++ b/client/onboarding/types.ts
@@ -2,7 +2,7 @@
* Internal dependencies
*/
-export type OnboardingSteps = 'business' | 'store' | 'loading';
+export type OnboardingSteps = 'business' | 'store' | 'embedded' | 'loading';
export type OnboardingFields = {
country?: string;
@@ -13,6 +13,15 @@ export type OnboardingFields = {
go_live_timeframe?: string;
};
+export interface OnboardingProps {
+ country?: string;
+ type?: string;
+ structure?: string;
+ mcc?: string;
+ annual_revenue?: string;
+ go_live_timeframe?: string;
+}
+
export interface PoEligibleResult {
result: 'eligible' | 'not_eligible';
}
@@ -55,3 +64,13 @@ export interface MccsDisplayTreeItem {
mcc?: number;
keywords?: string[];
}
+
+export interface AccountKycSession {
+ clientSecret: string;
+ expiresAt: number;
+ accountId: string;
+ isLive: boolean;
+ accountCreated: boolean;
+ publishableKey: string;
+ locale: string;
+}
diff --git a/client/overview/index.js b/client/overview/index.js
index 5a2cc84ba52..f6d5afd4a72 100644
--- a/client/overview/index.js
+++ b/client/overview/index.js
@@ -190,6 +190,7 @@ const OverviewPage = () => {
+
{ showConnectionSuccess && }
{ ! accountRejected && ! accountUnderReview && (
diff --git a/client/utils/index.js b/client/utils/index.js
index bc24bf44f4f..ba5b0f9493a 100644
--- a/client/utils/index.js
+++ b/client/utils/index.js
@@ -100,6 +100,40 @@ export const getDocumentUrl = ( documentId ) => {
);
};
+export const getConnectUrl = ( urlParams, from ) => {
+ // Ensure urlParams is an object.
+ const queryParams = typeof urlParams === 'object' ? urlParams : {};
+
+ const baseParams = {
+ page: 'wc-admin',
+ path: '/payments/connect',
+ source: queryParams.source?.replace( /[^\w-]+/g, '' ) || 'unknown',
+ from: from,
+ };
+
+ // Merge queryParams and baseParams into baseParams, ensuring baseParams takes precedence.
+ const params = { ...queryParams, ...baseParams };
+
+ return getAdminUrl( params );
+};
+
+export const getOverviewUrl = ( urlParams, from ) => {
+ // Ensure urlParams is an object.
+ const queryParams = typeof urlParams === 'object' ? urlParams : {};
+
+ const baseParams = {
+ page: 'wc-admin',
+ path: '/payments/overview',
+ source: queryParams.source?.replace( /[^\w-]+/g, '' ) || 'unknown',
+ from: from,
+ };
+
+ // Merge queryParams and baseParams into baseParams, ensuring baseParams takes precedence.
+ const params = { ...queryParams, ...baseParams };
+
+ return getAdminUrl( params );
+};
+
/**
* Returns the URL to the WooPayments settings.
*
diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php
index 448d85831b2..85f3439ba85 100644
--- a/includes/admin/class-wc-payments-admin.php
+++ b/includes/admin/class-wc-payments-admin.php
@@ -347,6 +347,23 @@ public function add_payments_menu() {
remove_submenu_page( 'wc-admin&path=/payments/connect', 'wc-admin&path=/payments/onboarding' );
}
+ // Register /payments/onboarding/kyc only when we have a Stripe account, but the Stripe KYC is not finished (details not submitted).
+ if ( WC_Payments_Features::is_embedded_kyc_enabled() && $this->account->is_stripe_connected() && ! $this->account->is_details_submitted() ) {
+ wc_admin_register_page(
+ [
+ 'id' => 'wc-payments-onboarding-kyc',
+ 'title' => __( 'Continue onboarding', 'woocommerce-payments' ),
+ 'parent' => 'wc-payments',
+ 'path' => '/payments/onboarding/kyc',
+ 'capability' => 'manage_woocommerce',
+ 'nav_args' => [
+ 'parent' => 'wc-payments',
+ ],
+ ]
+ );
+ remove_submenu_page( 'wc-admin&path=/payments/connect', 'wc-admin&path=/payments/onboarding/kyc' );
+ }
+
if ( $should_render_full_menu ) {
if ( $this->account->is_card_present_eligible() && $this->account->has_card_readers_available() ) {
$this->admin_child_pages['wc-payments-card-readers'] = [
@@ -598,6 +615,15 @@ public function enqueue_payments_scripts() {
wp_enqueue_style( 'WCPAY_ADMIN_SETTINGS' );
}
+ // Enqueue the onboarding scripts if the user is on the onboarding page.
+ if ( WC_Payments_Utils::is_onboarding_page() ) {
+ wp_localize_script(
+ 'WCPAY_ONBOARDING_SETTINGS',
+ 'wcpayOnboardingSettings',
+ []
+ );
+ }
+
// TODO: Try to enqueue the JS and CSS bundles lazily (will require changes on WC-Admin).
$current_screen = get_current_screen() ? get_current_screen()->base : null;
if ( wc_admin_is_registered_page() || 'widgets' === $current_screen ) {
@@ -849,6 +875,7 @@ private function get_js_settings(): array {
// Set this flag for use in the front-end to alter messages and notices if on-boarding has been disabled.
'onBoardingDisabled' => WC_Payments_Account::is_on_boarding_disabled(),
'onboardingFieldsData' => $this->onboarding_service->get_fields_data( get_user_locale() ),
+ 'onboardingEmbeddedKycInProgress' => $this->onboarding_service->is_embedded_kyc_in_progress(),
'errorMessage' => $error_message,
'featureFlags' => $this->get_frontend_feature_flags(),
'isSubscriptionsActive' => class_exists( 'WC_Subscriptions' ) && version_compare( WC_Subscriptions::$version, '2.2.0', '>=' ),
diff --git a/includes/admin/class-wc-rest-payments-onboarding-controller.php b/includes/admin/class-wc-rest-payments-onboarding-controller.php
index bfad476ce87..1a9553d46c8 100644
--- a/includes/admin/class-wc-rest-payments-onboarding-controller.php
+++ b/includes/admin/class-wc-rest-payments-onboarding-controller.php
@@ -5,8 +5,7 @@
* @package WooCommerce\Payments\Admin
*/
-use WCPay\Exceptions\API_Exception;
-use WCPay\Exceptions\Rest_Request_Exception;
+use WCPay\Logger;
defined( 'ABSPATH' ) || exit;
@@ -49,6 +48,87 @@ public function __construct(
* Configure REST API routes.
*/
public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/kyc/session',
+ [
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => [ $this, 'get_embedded_kyc_session' ],
+ 'permission_callback' => [ $this, 'check_permission' ],
+ 'args' => [
+ 'progressive' => [
+ 'required' => false,
+ 'description' => 'Whether the session is for progressive onboarding.',
+ 'type' => 'string',
+ ],
+ 'collect_payout_requirements' => [
+ 'required' => false,
+ 'description' => 'Whether the session is for collecting payout requirements.',
+ 'type' => 'string',
+ ],
+ 'self_assessment' => [
+ 'required' => false,
+ 'description' => 'The self-assessment data.',
+ 'type' => 'object',
+ 'properties' => [
+ 'country' => [
+ 'type' => 'string',
+ 'description' => 'The country code where the company is legally registered.',
+ 'required' => true,
+ ],
+ 'business_type' => [
+ 'type' => 'string',
+ 'description' => 'The company incorporation type.',
+ 'required' => true,
+ ],
+ 'mcc' => [
+ 'type' => 'string',
+ 'description' => 'The merchant category code. This can either be a true MCC or an MCCs tree item id from the onboarding form.',
+ 'required' => true,
+ ],
+ 'annual_revenue' => [
+ 'type' => 'string',
+ 'description' => 'The estimated annual revenue bucket id.',
+ 'required' => true,
+ ],
+ 'go_live_timeframe' => [
+ 'type' => 'string',
+ 'description' => 'The timeframe bucket for the estimated first live transaction.',
+ 'required' => true,
+ ],
+ 'url' => [
+ 'type' => 'string',
+ 'description' => 'The URL of the store.',
+ 'required' => true,
+ ],
+ ],
+ ],
+ ],
+ ]
+ );
+
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/kyc/finalize',
+ [
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => [ $this, 'finalize_embedded_kyc' ],
+ 'permission_callback' => [ $this, 'check_permission' ],
+ 'args' => [
+ 'source' => [
+ 'required' => false,
+ 'description' => 'The very first entry point the merchant entered our onboarding flow.',
+ 'type' => 'string',
+ ],
+ 'from' => [
+ 'required' => false,
+ 'description' => 'The previous step in the onboarding flow leading the merchant to arrive at the current step.',
+ 'type' => 'string',
+ ],
+ ],
+ ]
+ );
+
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/business_types',
@@ -116,6 +196,72 @@ public function register_routes() {
);
}
+ /**
+ * Create an account embedded KYC session via the API.
+ *
+ * @param WP_REST_Request $request Request object.
+ *
+ * @return WP_Error|WP_REST_Response
+ */
+ public function get_embedded_kyc_session( WP_REST_Request $request ) {
+ $account_session = $this->onboarding_service->create_embedded_kyc_session(
+ ! empty( $request->get_param( 'self_assessment' ) ) ? wc_clean( wp_unslash( $request->get_param( 'self_assessment' ) ) ) : [],
+ ! empty( $request->get_param( 'progressive' ) ) && 'true' === $request->get_param( 'progressive' ),
+ ! empty( $request->get_param( 'collect_payout_requirements' ) ) && 'true' === $request->get_param( 'collect_payout_requirements' )
+ );
+
+ if ( $account_session ) {
+ $account_session['locale'] = get_user_locale();
+ }
+
+ // Set the onboarding in progress option.
+ $this->onboarding_service->set_embedded_kyc_in_progress();
+
+ return rest_ensure_response( $account_session );
+ }
+
+ /**
+ * Finalize the embedded KYC session via the API.
+ *
+ * @param WP_REST_Request $request Request object.
+ *
+ * @return WP_Error|WP_HTTP_Response|WP_REST_Response
+ */
+ public function finalize_embedded_kyc( WP_REST_Request $request ) {
+ $source = $request->get_param( 'source' ) ?? '';
+ $from = $request->get_param( 'from' ) ?? '';
+ $actioned_notes = WC_Payments_Onboarding_Service::get_actioned_notes();
+
+ // Call the API to finalize the onboarding.
+ try {
+ $response = $this->onboarding_service->finalize_embedded_kyc(
+ get_user_locale(),
+ $source,
+ $actioned_notes
+ );
+ } catch ( Exception $e ) {
+ return new WP_Error( self::RESULT_BAD_REQUEST, $e->getMessage(), [ 'status' => 400 ] );
+ }
+
+ // Handle some post-onboarding tasks and get the redirect params.
+ $finalize = WC_Payments::get_account_service()->finalize_embedded_connection(
+ $response['mode'],
+ [
+ 'promo' => $response['promotion_id'] ?? '',
+ 'from' => $from,
+ 'source' => $source,
+ ]
+ );
+
+ // Return the response, the client will handle the redirect.
+ return rest_ensure_response(
+ array_merge(
+ $response,
+ $finalize
+ )
+ );
+ }
+
/**
* Get business types via API.
*
diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php
index 84620ca71c7..3f8fbd60789 100644
--- a/includes/class-wc-payments-account.php
+++ b/includes/class-wc-payments-account.php
@@ -9,8 +9,6 @@
exit; // Exit if accessed directly.
}
-use Automattic\WooCommerce\Admin\Notes\DataStore;
-use Automattic\WooCommerce\Admin\Notes\Note;
use WCPay\Constants\Country_Code;
use WCPay\Constants\Currency_Code;
use WCPay\Core\Server\Request\Get_Account;
@@ -30,6 +28,7 @@ class WC_Payments_Account {
const ONBOARDING_DISABLED_TRANSIENT = 'wcpay_on_boarding_disabled';
const ONBOARDING_STARTED_TRANSIENT = 'wcpay_on_boarding_started';
const ONBOARDING_STATE_TRANSIENT = 'wcpay_stripe_onboarding_state';
+ const EMBEDDED_KYC_IN_PROGRESS_OPTION = 'wcpay_onboarding_embedded_kyc_in_progress';
const ERROR_MESSAGE_TRANSIENT = 'wcpay_error_message';
const INSTANT_DEPOSITS_REMINDER_ACTION = 'wcpay_instant_deposit_reminder';
const TRACKS_EVENT_ACCOUNT_CONNECT_START = 'wcpay_account_connect_start';
@@ -61,11 +60,11 @@ class WC_Payments_Account {
private $action_scheduler_service;
/**
- * WC_Payments_Session_Service instance for working with session information
+ * WC_Payments_Onboarding_Service instance for working with onboarding business logic
*
- * @var WC_Payments_Session_Service
+ * @var WC_Payments_Onboarding_Service
*/
- private $session_service;
+ private $onboarding_service;
/**
* WC_Payments_Redirect_Service instance for handling redirects business logic
@@ -80,20 +79,20 @@ class WC_Payments_Account {
* @param WC_Payments_API_Client $payments_api_client Payments API client.
* @param Database_Cache $database_cache Database cache util.
* @param WC_Payments_Action_Scheduler_Service $action_scheduler_service Action scheduler service.
- * @param WC_Payments_Session_Service $session_service Session service.
+ * @param WC_Payments_Onboarding_Service $onboarding_service Onboarding service.
* @param WC_Payments_Redirect_Service $redirect_service Redirect service.
*/
public function __construct(
WC_Payments_API_Client $payments_api_client,
Database_Cache $database_cache,
WC_Payments_Action_Scheduler_Service $action_scheduler_service,
- WC_Payments_Session_Service $session_service,
+ WC_Payments_Onboarding_Service $onboarding_service,
WC_Payments_Redirect_Service $redirect_service
) {
$this->payments_api_client = $payments_api_client;
$this->database_cache = $database_cache;
$this->action_scheduler_service = $action_scheduler_service;
- $this->session_service = $session_service;
+ $this->onboarding_service = $onboarding_service;
$this->redirect_service = $redirect_service;
}
@@ -932,6 +931,25 @@ public function maybe_redirect_from_connect_page(): bool {
return false;
}
+ // There are certain cases where it is best to refresh the account data
+ // to be sure we are dealing with the current account state on the Connect page:
+ // - When the merchant is coming from the onboarding wizard it is best to refresh the account data because
+ // the merchant might have started the embedded Stripe KYC.
+ // - When the merchant is coming from the embedded KYC, definitely refresh the account data.
+ // The account data shouldn't be refreshed with force disconnected option enabled.
+ if ( ! WC_Payments_Utils::force_disconnected_enabled()
+ && in_array(
+ WC_Payments_Onboarding_Service::get_from(),
+ [
+ WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD,
+ WC_Payments_Onboarding_Service::FROM_ONBOARDING_KYC,
+ ],
+ true
+ ) ) {
+
+ $this->refresh_account_data();
+ }
+
// If everything is in good working condition, redirect to Payments Overview page.
if ( $this->has_working_jetpack_connection() && $this->is_stripe_account_valid() ) {
$this->redirect_service->redirect_to_overview_page( WC_Payments_Onboarding_Service::FROM_CONNECT_PAGE );
@@ -1174,6 +1192,7 @@ public function maybe_handle_onboarding() {
|| ( WC_Payments_Onboarding_Service::FROM_STRIPE === $from && ! empty( $_GET['wcpay-connection-error'] ) ) ) {
delete_transient( self::ONBOARDING_STATE_TRANSIENT );
+ delete_option( self::EMBEDDED_KYC_IN_PROGRESS_OPTION );
}
// Make changes to account data as instructed by action GET params.
@@ -1400,6 +1419,7 @@ public function maybe_handle_onboarding() {
if ( $create_test_drive_account ) {
// Since there should be no Stripe KYC needed, make sure we start with a clean state.
delete_transient( self::ONBOARDING_STATE_TRANSIENT );
+ delete_option( self::EMBEDDED_KYC_IN_PROGRESS_OPTION );
// If we have the auto_start_test_drive_onboarding flag, we redirect to the Connect page
// to let the JS logic take control and orchestrate things.
@@ -1482,7 +1502,7 @@ public function maybe_handle_onboarding() {
if ( $create_test_drive_account && ! empty( $redirect_to ) ) {
wp_send_json_success( [ 'redirect_to' => $redirect_to ] );
} else {
- // Redirect the user to where our Stripe onboarding instructed.
+ // Redirect the user to where our Stripe onboarding instructed (or to our own embedded Stripe KYC).
$this->redirect_service->redirect_to( $redirect_to );
}
} catch ( API_Exception $e ) {
@@ -1561,6 +1581,7 @@ private function cleanup_on_account_reset() {
// Discard any ongoing onboarding session.
delete_transient( self::ONBOARDING_STATE_TRANSIENT );
delete_transient( self::ONBOARDING_STARTED_TRANSIENT );
+ delete_option( self::EMBEDDED_KYC_IN_PROGRESS_OPTION );
delete_transient( 'woopay_enabled_by_default' );
// Clear the cache to avoid stale data.
@@ -1744,6 +1765,24 @@ private function get_onboarding_return_url( string $wcpay_connect_from ): string
}
}
+ /**
+ * Get the URL to the embedded onboarding KYC page.
+ *
+ * @param array $additional_args Additional query args to add to the URL.
+ *
+ * @return string
+ */
+ private function get_onboarding_kyc_url( array $additional_args = [] ): string {
+ $params = [
+ 'page' => 'wc-admin',
+ 'path' => '/payments/onboarding/kyc',
+ ];
+
+ $params = array_merge( $params, $additional_args );
+
+ return admin_url( add_query_arg( $params, 'admin.php' ) );
+ }
+
/**
* Initializes the onboarding flow by fetching the URL from the API and redirecting to it.
*
@@ -1775,83 +1814,29 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne
WC_Payments_Onboarding_Service::set_onboarding_eligibility_modal_dismissed();
}
+ // If we are in the middle of an embedded onboarding, go to the KYC page.
+ // In this case, we don't need to generate a return URL from Stripe, and we
+ // can rely on the JS logic to generate the session.
+ // Currently under feature flag.
+ if ( WC_Payments_Features::is_embedded_kyc_enabled() && $this->onboarding_service->is_embedded_kyc_in_progress() ) {
+ // We want to carry over the connect link from value because with embedded KYC
+ // there is no interim step for the user.
+ $additional_args['from'] = WC_Payments_Onboarding_Service::get_from();
+
+ return $this->get_onboarding_kyc_url( $additional_args );
+ }
+
+ // Else, go on with the normal onboarding redirect logic.
$return_url = $this->get_onboarding_return_url( $wcpay_connect_from );
if ( ! empty( $additional_args ) ) {
$return_url = add_query_arg( $additional_args, $return_url );
}
- $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.
- $home_is_localhost = 'localhost' === wp_parse_url( $home_url, PHP_URL_HOST );
- $fallback_url = ( 'live' !== $setup_mode || $home_is_localhost ) ? 'https://wcpay.test' : null;
-
- $current_user = get_userdata( get_current_user_id() );
-
- // The general account data.
- $account_data = [
- 'setup_mode' => $setup_mode,
- // We use the store base country to create a customized account.
- 'country' => WC()->countries->get_base_country() ?? null,
- 'url' => ! $home_is_localhost && wp_http_validate_url( $home_url ) ? $home_url : $fallback_url,
- 'business_name' => get_bloginfo( 'name' ),
- ];
-
- // Gather all the account data depending on the request context.
- // Onboarding self-assessment data.
$self_assessment_data = isset( $_GET['self_assessment'] ) ? wc_clean( wp_unslash( $_GET['self_assessment'] ) ) : [];
- if ( ! empty( $self_assessment_data ) ) {
- $business_type = $self_assessment_data['business_type'] ?? null;
- $account_data = WC_Payments_Utils::array_merge_recursive_distinct(
- $account_data,
- [
- // Overwrite the country if the merchant chose a different one than the Woo base location.
- 'country' => $self_assessment_data['country'] ?? null,
- 'email' => $self_assessment_data['email'] ?? null,
- 'business_name' => $self_assessment_data['business_name'] ?? null,
- 'url' => $self_assessment_data['url'] ?? null,
- 'mcc' => $self_assessment_data['mcc'] ?? null,
- 'business_type' => $business_type,
- 'company' => [
- 'structure' => 'company' === $business_type ? ( $self_assessment_data['company']['structure'] ?? null ) : null,
- ],
- 'individual' => [
- 'first_name' => $self_assessment_data['individual']['first_name'] ?? null,
- 'last_name' => $self_assessment_data['individual']['last_name'] ?? null,
- 'phone' => $self_assessment_data['phone'] ?? null,
- ],
- 'store' => [
- 'annual_revenue' => $self_assessment_data['annual_revenue'] ?? null,
- 'go_live_timeframe' => $self_assessment_data['go_live_timeframe'] ?? null,
- ],
- ]
- );
- } elseif ( 'test_drive' === $setup_mode ) {
- $account_data = WC_Payments_Utils::array_merge_recursive_distinct(
- $account_data,
- [
- 'individual' => [
- 'first_name' => $current_user->first_name ?? null,
- 'last_name' => $current_user->last_name ?? null,
- ],
- ]
- );
-
+ 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 );
} elseif ( 'test' === $setup_mode ) {
- $account_data = WC_Payments_Utils::array_merge_recursive_distinct(
- $account_data,
- [
- 'business_type' => 'individual',
- 'mcc' => '5734',
- 'individual' => [
- 'first_name' => $current_user->first_name ?? null,
- 'last_name' => $current_user->last_name ?? null,
- ],
- ]
- );
-
// 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 );
}
@@ -1861,7 +1846,8 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne
'site_locale' => get_locale(),
];
- $user_data = $this->get_onboarding_user_data();
+ $user_data = $this->onboarding_service->get_onboarding_user_data();
+ $account_data = $this->onboarding_service->get_account_data( $setup_mode, $self_assessment_data );
$onboarding_data = $this->payments_api_client->get_onboarding_data(
'live' === $setup_mode,
@@ -1869,7 +1855,7 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne
$site_data,
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.
- $this->get_actioned_notes(),
+ WC_Payments_Onboarding_Service::get_actioned_notes(),
$progressive,
$collect_payout_requirements
);
@@ -1888,6 +1874,7 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne
// Clean up any existing onboarding state.
delete_transient( self::ONBOARDING_STATE_TRANSIENT );
+ delete_option( self::EMBEDDED_KYC_IN_PROGRESS_OPTION );
return add_query_arg(
[ 'wcpay-connection-success' => '1' ],
@@ -1920,6 +1907,50 @@ public function maybe_activate_woopay() {
}
}
+ /**
+ * Handle the finalization of an embedded onboarding. This includes updating the cache, setting the gateway mode,
+ * tracking the event, and redirecting the user to the overview page.
+ *
+ * @param string $mode The mode in which the account was created. Either 'test' or 'live'.
+ * @param array $additional_args Additional query args to add to the redirect URLs.
+ *
+ * @return array Returns whether the operation was successful, along with the URL params to handle the redirect.
+ */
+ public function finalize_embedded_connection( string $mode, array $additional_args = [] ): array {
+ // Clear the account cache.
+ $this->clear_cache();
+
+ // Set the gateway options.
+ $gateway = WC_Payments::get_gateway();
+ $gateway->update_option( 'enabled', 'yes' );
+ $gateway->update_option( 'test_mode', 'live' !== $mode ? 'yes' : 'no' );
+
+ // Store a state after completing KYC for tracks. This is stored temporarily in option because
+ // user might not have agreed to TOS yet.
+ update_option( '_wcpay_onboarding_stripe_connected', [ 'is_existing_stripe_account' => false ] );
+
+ // Track account connection finish.
+ $event_properties = [
+ 'mode' => 'test' === $mode ? 'test' : 'live',
+ 'incentive' => ! empty( $additional_args['promo'] ) ? sanitize_text_field( $additional_args['promo'] ) : '',
+ 'from' => ! empty( $additional_args['from'] ) ? sanitize_text_field( $additional_args['from'] ) : '',
+ 'source' => ! empty( $additional_args['source'] ) ? sanitize_text_field( $additional_args['source'] ) : '',
+ ];
+
+ $this->tracks_event(
+ self::TRACKS_EVENT_ACCOUNT_CONNECT_FINISHED,
+ $event_properties
+ );
+
+ $params = $additional_args;
+
+ $params['wcpay-connection-success'] = '1';
+ return [
+ 'success' => true,
+ 'params' => $params,
+ ];
+ }
+
/**
* Once the API redirects back to the site after the onboarding flow, verifies the parameters and stores the data.
*
@@ -2204,59 +2235,6 @@ public function get_latest_tos_agreement() {
: null;
}
- /**
- * Returns an array containing the names of all the WCPay related notes that have been actioned.
- *
- * @return array
- */
- private function get_actioned_notes(): array {
- $wcpay_note_names = [];
-
- try {
- /**
- * Data Store for admin notes
- *
- * @var DataStore $data_store
- */
- $data_store = WC_Data_Store::load( 'admin-note' );
- } catch ( Exception $e ) {
- // Don't stop the on-boarding process if something goes wrong here. Log the error and return the empty array
- // of actioned notes.
- Logger::error( $e );
- return $wcpay_note_names;
- }
-
- // Fetch the last 10 actioned wcpay-promo admin notifications.
- $add_like_clause = function ( $where_clause ) {
- return $where_clause . " AND name like 'wcpay-promo-%'";
- };
-
- add_filter( 'woocommerce_note_where_clauses', $add_like_clause );
-
- $wcpay_promo_notes = $data_store->get_notes(
- [
- 'status' => [ Note::E_WC_ADMIN_NOTE_ACTIONED ],
- 'is_deleted' => false,
- 'per_page' => 10,
- ]
- );
-
- remove_filter( 'woocommerce_note_where_clauses', $add_like_clause );
-
- // If we didn't get an array back from the data store, return an empty array of results.
- if ( ! is_array( $wcpay_promo_notes ) ) {
- return $wcpay_note_names;
- }
-
- // Copy the name of each note into the results.
- foreach ( (array) $wcpay_promo_notes as $wcpay_note ) {
- $note = new Note( $wcpay_note->note_id );
- $wcpay_note_names[] = $note->get_name();
- }
-
- return $wcpay_note_names;
- }
-
/**
* Gets the account country.
*
@@ -2455,24 +2433,4 @@ public function get_lifetime_total_payment_volume(): int {
$account = $this->get_cached_account_data();
return (int) ! empty( $account ) && isset( $account['lifetime_total_payment_volume'] ) ? $account['lifetime_total_payment_volume'] : 0;
}
-
- /**
- * Get user data to send to the onboarding flow.
- *
- * @return array The user data.
- */
- private function get_onboarding_user_data(): array {
- return [
- 'user_id' => get_current_user_id(),
- 'sift_session_id' => $this->session_service->get_sift_session_id(),
- 'ip_address' => \WC_Geolocation::get_ip_address(),
- 'browser' => [
- 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
- 'accept_language' => isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '',
- 'content_language' => empty( get_user_locale() ) ? 'en-US' : str_replace( '_', '-', get_user_locale() ),
- ],
- 'referer' => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '',
- 'onboarding_source' => WC_Payments_Onboarding_Service::get_source(),
- ];
- }
}
diff --git a/includes/class-wc-payments-features.php b/includes/class-wc-payments-features.php
index 161cd8e3935..8df7d249206 100644
--- a/includes/class-wc-payments-features.php
+++ b/includes/class-wc-payments-features.php
@@ -32,6 +32,7 @@ class WC_Payments_Features {
const TOKENIZED_CART_PRB_FLAG_NAME = '_wcpay_feature_tokenized_cart_prb';
const PAYMENT_OVERVIEW_WIDGET_FLAG_NAME = '_wcpay_feature_payment_overview_widget';
const WOOPAY_GLOBAL_THEME_SUPPORT_FLAG_NAME = '_wcpay_feature_woopay_global_theme_support';
+ const EMBEDDED_KYC_FLAG_NAME = '_wcpay_feature_embedded_kyc';
/**
* Indicates whether card payments are enabled for this (Stripe) account.
@@ -75,6 +76,15 @@ public static function is_customer_multi_currency_enabled() {
return '1' === get_option( '_wcpay_feature_customer_multi_currency', '1' );
}
+ /**
+ * Checks whether Embedded KYC is enabled.
+ *
+ * @return bool
+ */
+ public static function is_embedded_kyc_enabled(): bool {
+ return '1' === get_option( self::EMBEDDED_KYC_FLAG_NAME, '0' );
+ }
+
/**
* Checks whether WCPay Subscriptions is enabled.
*
@@ -387,6 +397,7 @@ public static function to_array() {
'isDisputeIssuerEvidenceEnabled' => self::is_dispute_issuer_evidence_enabled(),
'isPaymentOverviewWidgetEnabled' => self::is_payment_overview_widget_ui_enabled(),
'isStripeEceEnabled' => self::is_stripe_ece_enabled(),
+ 'isEmbeddedKycEnabled' => self::is_embedded_kyc_enabled(),
]
);
}
diff --git a/includes/class-wc-payments-onboarding-service.php b/includes/class-wc-payments-onboarding-service.php
index 56bb49f6607..e4332f42ab9 100644
--- a/includes/class-wc-payments-onboarding-service.php
+++ b/includes/class-wc-payments-onboarding-service.php
@@ -9,8 +9,11 @@
exit; // Exit if accessed directly.
}
+use Automattic\WooCommerce\Admin\Notes\DataStore;
+use Automattic\WooCommerce\Admin\Notes\Note;
use WCPay\Database_Cache;
use WCPay\Exceptions\API_Exception;
+use WCPay\Logger;
/**
* Class handling onboarding related business logic.
@@ -52,6 +55,7 @@ class WC_Payments_Onboarding_Service {
const FROM_OVERVIEW_PAGE = 'WCPAY_OVERVIEW';
const FROM_ACCOUNT_DETAILS = 'WCPAY_ACCOUNT_DETAILS';
const FROM_ONBOARDING_WIZARD = 'WCPAY_ONBOARDING_WIZARD';
+ const FROM_ONBOARDING_KYC = 'WCPAY_ONBOARDING_KYC'; // The embedded Stripe KYC step/page.
const FROM_SETTINGS = 'WCPAY_SETTINGS';
const FROM_PAYOUTS = 'WCPAY_PAYOUTS';
const FROM_TEST_TO_LIVE = 'WCPAY_TEST_TO_LIVE';
@@ -62,6 +66,7 @@ class WC_Payments_Onboarding_Service {
const FROM_WPCOM = 'WPCOM';
const FROM_WPCOM_CONNECTION = 'WPCOM_CONNECTION';
const FROM_STRIPE = 'STRIPE';
+ const FROM_STRIPE_EMBEDDED = 'STRIPE_EMBEDDED';
/**
* Client for making requests to the WooCommerce Payments API
@@ -77,15 +82,24 @@ class WC_Payments_Onboarding_Service {
*/
private $database_cache;
+ /**
+ * Session service.
+ *
+ * @var WC_Payments_Session_Service instance for working with session information
+ */
+ private $session_service;
+
/**
* Class constructor
*
- * @param WC_Payments_API_Client $payments_api_client Payments API client.
- * @param Database_Cache $database_cache Database cache util.
+ * @param WC_Payments_API_Client $payments_api_client Payments API client.
+ * @param Database_Cache $database_cache Database cache util.
+ * @param WC_Payments_Session_Service $session_service Session service.
*/
- public function __construct( WC_Payments_API_Client $payments_api_client, Database_Cache $database_cache ) {
+ public function __construct( WC_Payments_API_Client $payments_api_client, Database_Cache $database_cache, WC_Payments_Session_Service $session_service ) {
$this->payments_api_client = $payments_api_client;
$this->database_cache = $database_cache;
+ $this->session_service = $session_service;
}
/**
@@ -136,6 +150,109 @@ function () use ( $locale ) {
);
}
+ /**
+ * Retrieve the embedded KYC session and handle initial account creation (if necessary).
+ *
+ * Will return the session key used to initialise the embedded onboarding session.
+ *
+ * @param array $self_assessment_data Self assessment data.
+ * @param boolean $progressive Whether the onboarding is progressive.
+ * @param boolean $collect_payout_requirements Whether to collect payout requirements.
+ *
+ * @return array Session data.
+ *
+ * @throws API_Exception
+ */
+ public function create_embedded_kyc_session( array $self_assessment_data, bool $progressive = false, bool $collect_payout_requirements = false ): array {
+ if ( ! $this->payments_api_client->is_server_connected() ) {
+ return [];
+ }
+ $setup_mode = WC_Payments::mode()->is_live() ? 'live' : 'test';
+
+ // Make sure the onboarding test mode DB flag is set.
+ self::set_test_mode( 'live' !== $setup_mode );
+
+ if ( ! $collect_payout_requirements ) {
+ // Clear onboarding related account options if this is an initial onboarding attempt.
+ self::clear_account_options();
+ } else {
+ // Since we assume user has already either gotten here from the eligibility modal,
+ // or has already dismissed it, we should set the modal as dismissed so it doesn't display again.
+ self::set_onboarding_eligibility_modal_dismissed();
+ }
+
+ $site_data = [
+ 'site_username' => wp_get_current_user()->user_login,
+ 'site_locale' => get_locale(),
+ ];
+ $user_data = $this->get_onboarding_user_data();
+ $account_data = $this->get_account_data( $setup_mode, $self_assessment_data );
+ $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.
+ $actioned_notes,
+ $progressive,
+ $collect_payout_requirements
+ );
+ } catch ( API_Exception $e ) {
+ // If we fail to create the session, return an empty array.
+ return [];
+ }
+
+ return [
+ 'clientSecret' => $account_session['client_secret'] ?? '',
+ 'expiresAt' => $account_session['expires_at'] ?? 0,
+ 'accountId' => $account_session['account_id'] ?? '',
+ 'isLive' => $account_session['is_live'] ?? false,
+ 'accountCreated' => $account_session['account_created'] ?? false,
+ 'publishableKey' => $account_session['publishable_key'] ?? '',
+ ];
+ }
+
+ /**
+ * Finalize the embedded KYC session.
+ *
+ * @param string $locale The locale to use to i18n the data.
+ * @param string $source The source of the onboarding flow.
+ * @param array $actioned_notes The actioned notes for this onboarding.
+ *
+ * @return array Containing the following keys: success, account_id, mode.
+ *
+ * @throws API_Exception
+ */
+ public function finalize_embedded_kyc( string $locale, string $source, array $actioned_notes ): array {
+ if ( ! $this->payments_api_client->is_server_connected() ) {
+ return [
+ 'success' => false,
+ ];
+ }
+
+ $result = $this->payments_api_client->finalize_onboarding_embedded_kyc( $locale, $source, $actioned_notes );
+
+ $success = $result['success'] ?? false;
+ $details_submitted = $result['details_submitted'] ?? false;
+
+ if ( ! $result || ! $success ) {
+ throw new API_Exception( __( 'Failed to finalize onboarding session.', 'woocommerce-payments' ), 'wcpay-onboarding-finalize-error', 400 );
+ }
+
+ // Clear the onboarding in progress option, since the onboarding flow is now complete.
+ $this->clear_embedded_kyc_in_progress();
+
+ return [
+ 'success' => $success,
+ 'details_submitted' => $details_submitted,
+ 'account_id' => $result['account_id'] ?? '',
+ 'mode' => $result['mode'],
+ 'promotion_id' => $result['promotion_id'] ?? null,
+ ];
+ }
+
/**
* Gets and caches the business types per country from the server.
*
@@ -215,6 +332,183 @@ public function add_admin_body_classes( string $classes = '' ): string {
return $classes;
}
+ /**
+ * Get account data for onboarding from self assestment data.
+ *
+ * @param string $setup_mode Setup mode.
+ * @param array $self_assessment_data Self assessment data.
+ *
+ * @return array Account data.
+ */
+ public function get_account_data( string $setup_mode, array $self_assessment_data ): 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.
+ $home_is_localhost = 'localhost' === wp_parse_url( $home_url, PHP_URL_HOST );
+ $fallback_url = ( 'live' !== $setup_mode || $home_is_localhost ) ? 'https://wcpay.test' : null;
+ $current_user = get_userdata( get_current_user_id() );
+
+ // The general account data.
+ $account_data = [
+ 'setup_mode' => $setup_mode,
+ // We use the store base country to create a customized account.
+ 'country' => WC()->countries->get_base_country() ?? null,
+ 'url' => ! $home_is_localhost && wp_http_validate_url( $home_url ) ? $home_url : $fallback_url,
+ 'business_name' => get_bloginfo( 'name' ),
+ ];
+
+ if ( ! empty( $self_assessment_data ) ) {
+ $business_type = $self_assessment_data['business_type'] ?? null;
+ $account_data = WC_Payments_Utils::array_merge_recursive_distinct(
+ $account_data,
+ [
+ // Overwrite the country if the merchant chose a different one than the Woo base location.
+ 'country' => $self_assessment_data['country'] ?? null,
+ 'email' => $self_assessment_data['email'] ?? null,
+ 'business_name' => $self_assessment_data['business_name'] ?? null,
+ 'url' => $self_assessment_data['url'] ?? null,
+ 'mcc' => $self_assessment_data['mcc'] ?? null,
+ 'business_type' => $business_type,
+ 'company' => [
+ 'structure' => 'company' === $business_type ? ( $self_assessment_data['company']['structure'] ?? null ) : null,
+ ],
+ 'individual' => [
+ 'first_name' => $self_assessment_data['individual']['first_name'] ?? null,
+ 'last_name' => $self_assessment_data['individual']['last_name'] ?? null,
+ 'phone' => $self_assessment_data['phone'] ?? null,
+ ],
+ 'store' => [
+ 'annual_revenue' => $self_assessment_data['annual_revenue'] ?? null,
+ 'go_live_timeframe' => $self_assessment_data['go_live_timeframe'] ?? null,
+ ],
+ ]
+ );
+ } elseif ( 'test_drive' === $setup_mode ) {
+ $account_data = WC_Payments_Utils::array_merge_recursive_distinct(
+ $account_data,
+ [
+ 'individual' => [
+ 'first_name' => $current_user->first_name ?? null,
+ 'last_name' => $current_user->last_name ?? null,
+ ],
+ ]
+ );
+ } elseif ( 'test' === $setup_mode ) {
+ $account_data = WC_Payments_Utils::array_merge_recursive_distinct(
+ $account_data,
+ [
+ 'business_type' => 'individual',
+ 'mcc' => '5734',
+ 'individual' => [
+ 'first_name' => $current_user->first_name ?? null,
+ 'last_name' => $current_user->last_name ?? null,
+ ],
+ ]
+ );
+ }
+ return $account_data;
+ }
+
+ /**
+ * Get user data to send to the onboarding flow.
+ *
+ * @return array The user data.
+ */
+ public function get_onboarding_user_data(): array {
+ return [
+ 'user_id' => get_current_user_id(),
+ 'sift_session_id' => $this->session_service->get_sift_session_id(),
+ 'ip_address' => \WC_Geolocation::get_ip_address(),
+ 'browser' => [
+ 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
+ 'accept_language' => isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '',
+ 'content_language' => empty( get_user_locale() ) ? 'en-US' : str_replace( '_', '-', get_user_locale() ),
+ ],
+ 'referer' => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '',
+ 'onboarding_source' => self::get_source(),
+ ];
+ }
+
+ /**
+ * Determine whether an embedded KYC flow is in progress.
+ *
+ * @return bool True if embedded KYC is in progress, false otherwise.
+ */
+ public function is_embedded_kyc_in_progress(): bool {
+ return in_array( get_option( WC_Payments_Account::EMBEDDED_KYC_IN_PROGRESS_OPTION, 'no' ), [ 'yes', '1' ], true );
+ }
+
+ /**
+ * Mark the embedded KYC flow as in progress.
+ *
+ * @return bool Whether we successfully marked the flow as in progress.
+ */
+ public function set_embedded_kyc_in_progress(): bool {
+ return update_option( WC_Payments_Account::EMBEDDED_KYC_IN_PROGRESS_OPTION, 'yes' );
+ }
+
+ /**
+ * Clear any embedded KYC in progress flags.
+ *
+ * @return boolean Whether we successfully cleared the flags.
+ */
+ public function clear_embedded_kyc_in_progress(): bool {
+ return delete_option( WC_Payments_Account::EMBEDDED_KYC_IN_PROGRESS_OPTION );
+ }
+
+ /**
+ * Get actioned notes.
+ *
+ * @return array
+ */
+ public static function get_actioned_notes(): array {
+ $wcpay_note_names = [];
+
+ try {
+ /**
+ * Data Store for admin notes
+ *
+ * @var DataStore $data_store
+ */
+ $data_store = WC_Data_Store::load( 'admin-note' );
+ } catch ( Exception $e ) {
+ // Don't stop the on-boarding process if something goes wrong here. Log the error and return the empty array
+ // of actioned notes.
+ Logger::error( $e );
+ return $wcpay_note_names;
+ }
+
+ // Fetch the last 10 actioned wcpay-promo admin notifications.
+ $add_like_clause = function ( $where_clause ) {
+ return $where_clause . " AND name like 'wcpay-promo-%'";
+ };
+
+ add_filter( 'woocommerce_note_where_clauses', $add_like_clause );
+
+ $wcpay_promo_notes = $data_store->get_notes(
+ [
+ 'status' => [ Note::E_WC_ADMIN_NOTE_ACTIONED ],
+ 'is_deleted' => false,
+ 'per_page' => 10,
+ ]
+ );
+
+ remove_filter( 'woocommerce_note_where_clauses', $add_like_clause );
+
+ // If we didn't get an array back from the data store, return an empty array of results.
+ if ( ! is_array( $wcpay_promo_notes ) ) {
+ return $wcpay_note_names;
+ }
+
+ // Copy the name of each note into the results.
+ foreach ( (array) $wcpay_promo_notes as $wcpay_note ) {
+ $note = new Note( $wcpay_note->note_id );
+ $wcpay_note_names[] = $note->get_name();
+ }
+
+ return $wcpay_note_names;
+ }
+
/**
* Clear any account options we may want to reset when a new onboarding flow is initialised.
* Currently, just deletes the option which stores whether the eligibility modal has been dismissed.
diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php
index 4593fe65019..0b2506d73b6 100644
--- a/includes/class-wc-payments-utils.php
+++ b/includes/class-wc-payments-utils.php
@@ -554,6 +554,19 @@ public static function is_payments_settings_page(): bool {
);
}
+ /**
+ * Checks if the currently displayed page is the WooPayments onboarding page.
+ *
+ * @return bool
+ */
+ public static function is_onboarding_page(): bool {
+ return (
+ is_admin()
+ && isset( $_GET['page'] ) && 'wc-admin' === $_GET['page'] // phpcs:ignore WordPress.Security.NonceVerification
+ && isset( $_GET['path'] ) && '/payments/onboarding' === $_GET['path'] // phpcs:ignore WordPress.Security.NonceVerification
+ );
+ }
+
/**
* Converts a locale to the closest supported by Stripe.js.
*
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index 2a5f8711bec..cd8854afa93 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -505,7 +505,8 @@ public static function init() {
self::$action_scheduler_service = new WC_Payments_Action_Scheduler_Service( self::$api_client, self::$order_service );
self::$session_service = new WC_Payments_Session_Service( self::$api_client );
self::$redirect_service = new WC_Payments_Redirect_Service( self::$api_client );
- self::$account = new WC_Payments_Account( self::$api_client, self::$database_cache, self::$action_scheduler_service, self::$session_service, self::$redirect_service );
+ self::$onboarding_service = new WC_Payments_Onboarding_Service( self::$api_client, self::$database_cache, self::$session_service );
+ self::$account = new WC_Payments_Account( self::$api_client, self::$database_cache, self::$action_scheduler_service, self::$onboarding_service, self::$redirect_service );
self::$customer_service = new WC_Payments_Customer_Service( self::$api_client, self::$account, self::$database_cache, self::$session_service, self::$order_service );
self::$token_service = new WC_Payments_Token_Service( self::$api_client, self::$customer_service );
self::$remote_note_service = new WC_Payments_Remote_Note_Service( WC_Data_Store::load( 'admin-note' ) );
@@ -514,7 +515,6 @@ public static function init() {
self::$localization_service = new WC_Payments_Localization_Service();
self::$failed_transaction_rate_limiter = new Session_Rate_Limiter( Session_Rate_Limiter::SESSION_KEY_DECLINED_CARD_REGISTRY, 5, 10 * MINUTE_IN_SECONDS );
self::$order_success_page = new WC_Payments_Order_Success_Page();
- self::$onboarding_service = new WC_Payments_Onboarding_Service( self::$api_client, self::$database_cache );
self::$woopay_util = new WooPay_Utilities();
self::$woopay_tracker = new WooPay_Tracker( self::get_wc_payments_http() );
self::$incentives_service = new WC_Payments_Incentives_Service( self::$database_cache );
diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php
index d360b76f07f..3ec8bc40e4b 100644
--- a/includes/wc-payment-api/class-wc-payments-api-client.php
+++ b/includes/wc-payment-api/class-wc-payments-api-client.php
@@ -970,6 +970,64 @@ public function get_onboarding_data( bool $live_account, string $return_url, arr
return $this->request( $request_args, self::ONBOARDING_API . '/init', self::POST, true, true );
}
+ /**
+ * Initialize the onboarding embedded KYC flow, returning a session object which is used by the frontend.
+ *
+ * @param bool $live_account Whether to create live account.
+ * @param array $site_data Site data.
+ * @param array $user_data User data.
+ * @param array $account_data Account data to be prefilled.
+ * @param array $actioned_notes Actioned notes to be sent.
+ * @param bool $progressive Whether progressive onboarding should be enabled for this onboarding.
+ * @param bool $collect_payout_requirements Whether we need to collect payout requirements.
+ *
+ * @return array
+ *
+ * @throws API_Exception
+ */
+ public function initialize_onboarding_embedded_kyc( bool $live_account, array $site_data = [], array $user_data = [], array $account_data = [], array $actioned_notes = [], bool $progressive = false, bool $collect_payout_requirements = false ): array {
+ $request_args = apply_filters(
+ 'wc_payments_get_onboarding_data_args',
+ [
+ 'site_data' => $site_data,
+ 'user_data' => $user_data,
+ 'account_data' => $account_data,
+ 'actioned_notes' => $actioned_notes,
+ 'create_live_account' => $live_account,
+ 'progressive' => $progressive,
+ 'collect_payout_requirements' => $collect_payout_requirements,
+ ]
+ );
+
+ $session = $this->request( $request_args, self::ONBOARDING_API . '/embedded', self::POST, true, true );
+
+ if ( ! is_array( $session ) ) {
+ return [];
+ }
+
+ return $session;
+ }
+
+ /**
+ * Finalize the onboarding embedded KYC flow.
+ *
+ * @param string $locale The locale to use to i18n the data.
+ * @param string $source The source of the onboarding flow.
+ * @param array $actioned_notes The actioned notes on the account related to this onboarding.
+ * @return array
+ *
+ * @throws API_Exception
+ */
+ public function finalize_onboarding_embedded_kyc( string $locale, string $source, array $actioned_notes ): array {
+ $request_args = [
+ 'locale' => $locale,
+ 'source' => $source,
+ 'actioned_notes' => $actioned_notes,
+ ];
+
+ return $this->request( $request_args, self::ONBOARDING_API . '/embedded/finalize', self::POST, true, true );
+ }
+
/**
* Get the fields data to be used by the onboarding flow.
*
diff --git a/package-lock.json b/package-lock.json
index 096544d3a4a..03592cffe24 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,8 @@
"dependencies": {
"@automattic/interpolate-components": "1.2.1",
"@fingerprintjs/fingerprintjs": "3.4.1",
+ "@stripe/connect-js": "3.3.12",
+ "@stripe/react-connect-js": "3.3.13",
"@stripe/react-stripe-js": "2.5.1",
"@stripe/stripe-js": "1.15.1",
"@woocommerce/explat": "2.3.0",
@@ -9155,6 +9157,21 @@
}
}
},
+ "node_modules/@stripe/connect-js": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/@stripe/connect-js/-/connect-js-3.3.12.tgz",
+ "integrity": "sha512-hXbgvGq9Lb6BYgsb8lcbjL76Yqsxr0yAj6T9ZFTfUK0O4otI5GSEWum9do9rf/E5OfYy6fR1FG/77Jve2w1o6Q=="
+ },
+ "node_modules/@stripe/react-connect-js": {
+ "version": "3.3.13",
+ "resolved": "https://registry.npmjs.org/@stripe/react-connect-js/-/react-connect-js-3.3.13.tgz",
+ "integrity": "sha512-kMxYjeQUcl/ixu/mSeX5QGIr/MuP+YxFSEBdb8j6w+tbK82tmcjyFDgoQTQwVXNqUV6jI66Kks3XcfpPRfeiJA==",
+ "peerDependencies": {
+ "@stripe/connect-js": ">=3.3.11",
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/@stripe/react-stripe-js": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.5.1.tgz",
diff --git a/package.json b/package.json
index e77b6facd24..6f11833d574 100644
--- a/package.json
+++ b/package.json
@@ -77,6 +77,8 @@
"dependencies": {
"@automattic/interpolate-components": "1.2.1",
"@fingerprintjs/fingerprintjs": "3.4.1",
+ "@stripe/connect-js": "3.3.12",
+ "@stripe/react-connect-js": "3.3.13",
"@stripe/react-stripe-js": "2.5.1",
"@stripe/stripe-js": "1.15.1",
"@woocommerce/explat": "2.3.0",
diff --git a/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php b/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php
index e7bc456c46b..07a00a2b690 100644
--- a/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php
+++ b/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php
@@ -140,4 +140,87 @@ public function test_get_progressive_onboarding_not_eligible() {
$response->get_data()
);
}
+
+ public function test_get_embedded_kyc_session() {
+ $kyc_session = [
+ 'clientSecret' => 'accs_secret__XXX',
+ 'expiresAt' => time() + 120,
+ 'accountId' => 'acct_XXX',
+ 'isLive' => false,
+ 'accountCreated' => true,
+ 'publishableKey' => 'pk_test_XXX',
+ ];
+
+ $this->mock_onboarding_service
+ ->expects( $this->once() )
+ ->method( 'create_embedded_kyc_session' )
+ ->willReturn(
+ $kyc_session
+ );
+
+ $this->mock_onboarding_service
+ ->expects( $this->once() )
+ ->method( 'set_embedded_kyc_in_progress' );
+
+ $request = new WP_REST_Request( 'GET' );
+ $request->set_query_params(
+ [
+ 'progressive' => true,
+ 'create_live_account' => true,
+ ]
+ );
+
+ $response = $this->controller->get_embedded_kyc_session( $request );
+ $this->assertSame( 200, $response->status );
+ $this->assertSame(
+ array_merge(
+ $kyc_session,
+ [
+ 'locale' => 'en_US',
+ ]
+ ),
+ $response->get_data()
+ );
+ }
+
+ public function test_finalize_embedded_kyc() {
+ $response_data = [
+ 'success' => true,
+ 'account_id' => 'acct_1PvxJQQujq4nxoo6',
+ 'details_submitted' => true,
+ 'mode' => 'test',
+ 'promotion_id' => null,
+ ];
+ $this->mock_onboarding_service
+ ->expects( $this->once() )
+ ->method( 'finalize_embedded_kyc' )
+ ->willReturn(
+ $response_data
+ );
+
+ $request = new WP_REST_Request( 'POST' );
+ $request->set_body_params(
+ [
+ 'source' => 'embedded',
+ 'from' => 'wcpay-connect',
+ ]
+ );
+
+ $response = $this->controller->finalize_embedded_kyc( $request );
+ $this->assertSame( 200, $response->status );
+ $this->assertSame(
+ array_merge(
+ $response_data,
+ [
+ 'params' => [
+ 'promo' => '',
+ 'from' => 'wcpay-connect',
+ 'source' => 'embedded',
+ 'wcpay-connection-success' => '1',
+ ],
+ ]
+ ),
+ $response->get_data()
+ );
+ }
}
diff --git a/tests/unit/test-class-wc-payments-account-capital.php b/tests/unit/test-class-wc-payments-account-capital.php
index 240d2bc2e51..cf48a9ceb26 100644
--- a/tests/unit/test-class-wc-payments-account-capital.php
+++ b/tests/unit/test-class-wc-payments-account-capital.php
@@ -50,11 +50,11 @@ class WC_Payments_Account_Capital_Test extends WCPAY_UnitTestCase {
private $mock_action_scheduler_service;
/**
- * Mock WC_Payments_Session_Service.
+ * Mock WC_Payments_Onboarding_Service.
*
- * @var WC_Payments_Session_Service|MockObject
+ * @var WC_Payments_Onboarding_Service|MockObject
*/
- private $mock_session_service;
+ private $mock_onboarding_service;
/**
* Mock WC_Payments_Redirect_Service.
@@ -80,13 +80,13 @@ public function set_up() {
$this->mock_api_client = $this->createMock( WC_Payments_API_Client::class );
$this->mock_database_cache = $this->createMock( Database_Cache::class );
$this->mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class );
- $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
+ $this->mock_onboarding_service = $this->createMock( WC_Payments_Onboarding_Service::class );
$this->mock_redirect_service = $this->createMock( WC_Payments_Redirect_Service::class );
// Mock WC_Payments_Account without redirect_to to prevent headers already sent error.
$this->wcpay_account = $this->getMockBuilder( WC_Payments_Account::class )
->setMethods( [ 'init_hooks' ] )
- ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service ] )
+ ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_onboarding_service, $this->mock_redirect_service ] )
->getMock();
$this->wcpay_account->init_hooks();
}
@@ -105,7 +105,7 @@ public function tear_down() {
public function test_maybe_redirect_by_get_param_will_run() {
$wcpay_account = $this->getMockBuilder( WC_Payments_Account::class )
->setMethodsExcept( [ 'init_hooks' ] )
- ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service ] )
+ ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_onboarding_service, $this->mock_redirect_service ] )
->getMock();
$wcpay_account->init_hooks();
diff --git a/tests/unit/test-class-wc-payments-account-link.php b/tests/unit/test-class-wc-payments-account-link.php
index 4ab2c92a8af..e8711f10b88 100644
--- a/tests/unit/test-class-wc-payments-account-link.php
+++ b/tests/unit/test-class-wc-payments-account-link.php
@@ -48,11 +48,11 @@ class WC_Payments_Account_Server_Links_Test extends WCPAY_UnitTestCase {
private $mock_action_scheduler_service;
/**
- * Mock WC_Payments_Session_Service.
+ * Mock WC_Payments_Onboarding_Service.
*
- * @var WC_Payments_Session_Service|MockObject
+ * @var WC_Payments_Onboarding_Service|MockObject
*/
- private $mock_session_service;
+ private $mock_onboarding_service;
/**
* Mock WC_Payments_Redirect_Service.
@@ -78,13 +78,13 @@ public function set_up() {
$this->mock_api_client = $this->createMock( WC_Payments_API_Client::class );
$this->mock_database_cache = $this->createMock( Database_Cache::class );
$this->mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class );
- $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
+ $this->mock_onboarding_service = $this->createMock( WC_Payments_Onboarding_Service::class );
$this->mock_redirect_service = $this->createMock( WC_Payments_Redirect_Service::class );
// Mock WC_Payments_Account without redirect_to to prevent headers already sent error.
$this->wcpay_account = $this->getMockBuilder( WC_Payments_Account::class )
->setMethods( [ 'init_hooks' ] )
- ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service ] )
+ ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_onboarding_service, $this->mock_redirect_service ] )
->getMock();
$this->wcpay_account->init_hooks();
diff --git a/tests/unit/test-class-wc-payments-account.php b/tests/unit/test-class-wc-payments-account.php
index 61215b93466..fa685451897 100644
--- a/tests/unit/test-class-wc-payments-account.php
+++ b/tests/unit/test-class-wc-payments-account.php
@@ -51,11 +51,11 @@ class WC_Payments_Account_Test extends WCPAY_UnitTestCase {
private $mock_action_scheduler_service;
/**
- * Mock WC_Payments_Session_Service.
+ * Mock WC_Payments_Onboarding_Service.
*
- * @var WC_Payments_Session_Service|MockObject
+ * @var WC_Payments_Onboarding_Service|MockObject
*/
- private $mock_session_service;
+ private $mock_onboarding_service;
/**
* Mock WC_Payments_Redirect_Service.
@@ -82,10 +82,10 @@ public function set_up() {
$this->mock_api_client = $this->createMock( WC_Payments_API_Client::class );
$this->mock_database_cache = $this->createMock( Database_Cache::class );
$this->mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class );
- $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
+ $this->mock_onboarding_service = $this->createMock( WC_Payments_Onboarding_Service::class );
$this->mock_redirect_service = $this->createMock( WC_Payments_Redirect_Service::class );
- $this->wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service );
+ $this->wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_onboarding_service, $this->mock_redirect_service );
$this->wcpay_account->init_hooks();
}
@@ -95,6 +95,7 @@ public function tear_down() {
unset( $_GET );
unset( $_REQUEST );
parent::tear_down();
+ delete_option( '_wcpay_feature_embedded_kyc' );
}
public function test_filters_registered_properly() {
@@ -268,7 +269,7 @@ public function test_maybe_handle_onboarding_connect_from_known_from(
->disableOriginalConstructor()
->onlyMethods( [ 'redirect_to' ] )
->getMock();
- $wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $mock_redirect_service );
+ $wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_onboarding_service, $mock_redirect_service );
$_GET['wcpay-connect'] = 'connect-from';
$_REQUEST['_wpnonce'] = wp_create_nonce( 'wcpay-connect' );
@@ -796,6 +797,71 @@ public function test_maybe_handle_onboarding_init_stripe_onboarding() {
$this->wcpay_account->maybe_handle_onboarding();
}
+ public function test_maybe_handle_onboarding_init_embedded_kyc() {
+ // Arrange.
+ // We need to be in the WP admin dashboard.
+ $this->set_is_admin( true );
+ // Test as an admin user.
+ wp_set_current_user( 1 );
+
+ $_GET['wcpay-connect'] = 'connect-from';
+ $_REQUEST['_wpnonce'] = wp_create_nonce( 'wcpay-connect' );
+ // Set the request as if the user is on some bogus page. It doesn't matter.
+ $_GET['page'] = 'wc-admin';
+ $_GET['path'] = '/payments/some-bogus-page';
+ // We need to come from the onboarding wizard to initialize an account!
+ $_GET['from'] = WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD;
+ $_GET['source'] = WC_Payments_Onboarding_Service::SOURCE_WCADMIN_INCENTIVE_PAGE;
+ // Make sure important flags are carried over.
+ $_GET['promo'] = 'incentive_id';
+ $_GET['progressive'] = 'true';
+ // There is no `test_mode` param and no test mode is set. It should end up as a live mode onboarding.
+
+ // The Jetpack connection is in working order.
+ $this->mock_jetpack_connection();
+
+ $this->mock_database_cache
+ ->expects( $this->any() )
+ ->method( 'get_or_add' )
+ ->willReturn( [] ); // Empty array means no Stripe account connected.
+
+ // Assert.
+ $this->mock_redirect_service
+ ->expects( $this->never() )
+ ->method( 'redirect_to_overview_page' );
+ $this->mock_redirect_service
+ ->expects( $this->never() )
+ ->method( 'redirect_to_connect_page' );
+ $this->mock_redirect_service
+ ->expects( $this->never() )
+ ->method( 'redirect_to_onboarding_wizard' );
+
+ update_option( '_wcpay_feature_embedded_kyc', '1' );
+
+ // If embedded KYC is in progress, we expect different URL.
+ $this->mock_onboarding_service
+ ->expects( $this->once() )
+ ->method( 'is_embedded_kyc_in_progress' )
+ ->willReturn( true );
+
+ $this->mock_api_client
+ ->expects( $this->never() )
+ ->method( 'get_onboarding_data' );
+
+ $this->mock_redirect_service
+ ->expects( $this->once() )
+ ->method( 'redirect_to' )
+ ->with(
+ $this->logicalOr(
+ $this->stringContains( 'page=wc-admin&path=/payments/onboarding/kyc' ),
+ $this->stringContains( 'page=wc-admin&path=%2Fpayments%2Fonboarding%2Fkyc' )
+ )
+ );
+
+ // Act.
+ $this->wcpay_account->maybe_handle_onboarding();
+ }
+
public function test_maybe_handle_onboarding_init_stripe_onboarding_existing_account() {
// Arrange.
// We need to be in the WP admin dashboard.
diff --git a/tests/unit/test-class-wc-payments-features.php b/tests/unit/test-class-wc-payments-features.php
index 217bb7be513..e9405375d8f 100644
--- a/tests/unit/test-class-wc-payments-features.php
+++ b/tests/unit/test-class-wc-payments-features.php
@@ -30,6 +30,7 @@ class WC_Payments_Features_Test extends WCPAY_UnitTestCase {
'_wcpay_feature_documents' => 'documents',
'_wcpay_feature_auth_and_capture' => 'isAuthAndCaptureEnabled',
'_wcpay_feature_stripe_ece' => 'isStripeEceEnabled',
+ '_wcpay_feature_embedded_kyc' => 'isEmbeddedKycEnabled',
];
public function set_up() {
@@ -300,6 +301,35 @@ public function test_is_frt_review_feature_active_returns_false_when_flag_is_not
$this->assertFalse( WC_Payments_Features::is_frt_review_feature_active() );
}
+ public function test_is_embedded_kyc_enabled_returns_true() {
+ add_filter(
+ 'pre_option_' . WC_Payments_Features::EMBEDDED_KYC_FLAG_NAME,
+ function ( $pre_option, $option, $default ) {
+ return '1';
+ },
+ 10,
+ 3
+ );
+ $this->assertTrue( WC_Payments_Features::is_embedded_kyc_enabled() );
+ }
+
+ public function test_is_embedded_kyc_enabled_returns_false_when_flag_is_false() {
+ add_filter(
+ 'pre_option_' . WC_Payments_Features::EMBEDDED_KYC_FLAG_NAME,
+ function ( $pre_option, $option, $default ) {
+ return '0';
+ },
+ 10,
+ 3
+ );
+ $this->assertFalse( WC_Payments_Features::is_embedded_kyc_enabled() );
+ $this->assertArrayNotHasKey( 'isEmbeddedKycEnabled', WC_Payments_Features::to_array() );
+ }
+
+ public function test_is_embedded_kyc_enabled_returns_false_when_flag_is_not_set() {
+ $this->assertFalse( WC_Payments_Features::is_embedded_kyc_enabled() );
+ }
+
private function setup_enabled_flags( array $enabled_flags ) {
foreach ( array_keys( self::FLAG_OPTION_NAME_TO_FRONTEND_KEY_MAPPING ) as $flag ) {
add_filter(
diff --git a/tests/unit/test-class-wc-payments-onboarding-service.php b/tests/unit/test-class-wc-payments-onboarding-service.php
index 7ec162205e4..a8d9686f763 100644
--- a/tests/unit/test-class-wc-payments-onboarding-service.php
+++ b/tests/unit/test-class-wc-payments-onboarding-service.php
@@ -34,6 +34,13 @@ class WC_Payments_Onboarding_Service_Test extends WCPAY_UnitTestCase {
*/
private $mock_database_cache;
+ /**
+ * Mock WC_Payments_Session_Service
+ *
+ * @var MockObject
+ */
+ private $mock_session_service;
+
/**
* Example business types array.
*
@@ -129,10 +136,11 @@ class WC_Payments_Onboarding_Service_Test extends WCPAY_UnitTestCase {
public function set_up() {
parent::set_up();
- $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class );
- $this->mock_database_cache = $this->createMock( Database_Cache::class );
+ $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class );
+ $this->mock_database_cache = $this->createMock( Database_Cache::class );
+ $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
- $this->onboarding_service = new WC_Payments_Onboarding_Service( $this->mock_api_client, $this->mock_database_cache );
+ $this->onboarding_service = new WC_Payments_Onboarding_Service( $this->mock_api_client, $this->mock_database_cache, $this->mock_session_service );
$this->onboarding_service->init_hooks();
}
@@ -202,6 +210,18 @@ public function test_set_test_mode() {
delete_option( WC_Payments_Onboarding_Service::TEST_MODE_OPTION );
}
+ public function test_is_embedded_kyc_in_progress() {
+ $this->assertFalse( $this->onboarding_service->is_embedded_kyc_in_progress() );
+
+ $this->onboarding_service->set_embedded_kyc_in_progress();
+
+ $this->assertTrue( $this->onboarding_service->is_embedded_kyc_in_progress() );
+
+ $this->onboarding_service->clear_embedded_kyc_in_progress();
+
+ $this->assertFalse( $this->onboarding_service->is_embedded_kyc_in_progress() );
+ }
+
/**
* @dataProvider data_get_from
*/