diff --git a/changelog/dev-8311-reduce-mox b/changelog/dev-8311-reduce-mox new file mode 100644 index 00000000000..3d300bbf6c0 --- /dev/null +++ b/changelog/dev-8311-reduce-mox @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Updates to reduce the amount of steps required during onboarding flow. diff --git a/client/components/custom-select-control/style.scss b/client/components/custom-select-control/style.scss index b8d13266489..3fb1b113f20 100644 --- a/client/components/custom-select-control/style.scss +++ b/client/components/custom-select-control/style.scss @@ -54,8 +54,8 @@ } .components-custom-select-control__menu { - margin: $gap 1px; - border-color: $gray-700; + margin: -1px 1px; + border-color: $gray-300; max-height: 300px; } } diff --git a/client/components/form/fields.tsx b/client/components/form/fields.tsx index 2ab6a6a9045..7868e067dc8 100644 --- a/client/components/form/fields.tsx +++ b/client/components/form/fields.tsx @@ -12,9 +12,6 @@ import CustomSelectControl, { ControlProps as SelectControlProps, Item as SelectItem, } from '../custom-select-control'; -import PhoneNumberControl, { - PhoneNumberControlProps, -} from '../phone-number-control'; import GroupedSelectControl, { GroupedSelectControlProps, ListItem as GroupedSelectItem, @@ -28,7 +25,6 @@ interface CommonProps { export type TextFieldProps = TextControl.Props & CommonProps; export type SelectFieldProps< ItemType > = SelectControlProps< ItemType > & CommonProps; -export type PhoneNumberFieldProps = PhoneNumberControlProps & CommonProps; export type GroupedSelectFieldProps< ItemType > = GroupedSelectControlProps< ItemType > & @@ -71,9 +67,6 @@ export const SelectField = < ItemType extends SelectItem >( props: SelectFieldProps< ItemType > ): JSX.Element => makeField( CustomSelectControl, props ); -export const PhoneNumberField: React.FC< PhoneNumberFieldProps > = ( props ) => - makeField( PhoneNumberControl, props ); - export const GroupedSelectField = < ItemType extends GroupedSelectItem >( props: GroupedSelectControlProps< ItemType > ): JSX.Element => makeField( GroupedSelectControl, props ); diff --git a/client/components/form/test/fields.tsx b/client/components/form/test/fields.tsx index 3e05bc7e4af..8bf79d4e9fb 100644 --- a/client/components/form/test/fields.tsx +++ b/client/components/form/test/fields.tsx @@ -7,12 +7,7 @@ import { render, screen } from '@testing-library/react'; /** * Internal dependencies */ -import { - TextField, - SelectField, - PhoneNumberField, - GroupedSelectField, -} from '../fields'; +import { TextField, SelectField, GroupedSelectField } from '../fields'; describe( 'Form fields components', () => { it( 'renders TextField component with provided props', () => { @@ -43,17 +38,6 @@ describe( 'Form fields components', () => { expect( screen.getByText( 'Test Label' ) ).toBeInTheDocument(); } ); - it( 'renders PhoneNumberField component with provided props', () => { - render( - - ); - expect( screen.getByText( 'Test Label' ) ).toBeInTheDocument(); - } ); - it( 'renders GroupedSelectField component with provided props', () => { const options = [ { key: 'option-1', name: 'Option 1', group: 'a' }, diff --git a/client/components/grouped-select-control/style.scss b/client/components/grouped-select-control/style.scss index 15819499fb0..e7e8ef2251a 100644 --- a/client/components/grouped-select-control/style.scss +++ b/client/components/grouped-select-control/style.scss @@ -29,19 +29,24 @@ &.placeholder { color: $gray-50; } + + @media screen and ( max-width: 782px ) { + font-size: 16px; + } } & &__search { + min-height: 36px; width: 100%; margin: 0; border: 0; padding: $gap-smaller $gap-small; border-radius: 0; - border-bottom: 1px solid $gray-700; + border-bottom: 1px solid $gray-300; &:focus { box-shadow: none; - border-color: $gray-700; + border-color: $gray-300; } } @@ -51,7 +56,7 @@ min-width: 100%; background-color: #fff; border-radius: 2px; - border: 1px solid $gray-700; + border: 1px solid $gray-300; margin: $gap 1px; z-index: 10000; display: flex; @@ -83,12 +88,14 @@ } &.is-group { - text-transform: uppercase; - color: $gray-40; - font-weight: 600; + color: $gray-700; } } + &__list { + margin: -1px 1px; + } + & &__search, &__list { font-size: 13px; diff --git a/client/components/inline-notice/index.tsx b/client/components/inline-notice/index.tsx index bb230170ab9..0b48b20e804 100644 --- a/client/components/inline-notice/index.tsx +++ b/client/components/inline-notice/index.tsx @@ -13,6 +13,7 @@ import { Action } from 'wcpay/types/notices'; * Internal dependencies. */ import './styles.scss'; +import ButtonVariant = Button.ButtonVariant; interface InlineNoticeProps extends Notice.Props { /** @@ -24,13 +25,20 @@ interface InlineNoticeProps extends Notice.Props { icon?: boolean | JSX.Element; actions?: readonly Action[] | undefined; + /** + * Allows more control over the button variant. + * Accepted values are 'primary', 'secondary', 'tertiary', and 'link'. + * + * @default undefined + */ + buttonVariant?: ButtonVariant; } /** * Renders a banner notice. */ function InlineNotice( props: InlineNoticeProps ): JSX.Element { - const { icon, actions, children, ...noticeProps } = props; + const { icon, actions, children, buttonVariant, ...noticeProps } = props; // Add the default class name to the notice. noticeProps.className = classNames( @@ -77,6 +85,7 @@ function InlineNotice( props: InlineNoticeProps ): JSX.Element { onClick={ action.onClick } isBusy={ action.isBusy ?? false } disabled={ action.disabled ?? false } + variant={ buttonVariant } > { action.label } diff --git a/client/components/inline-notice/styles.scss b/client/components/inline-notice/styles.scss index 94c5b954d80..dc9c5ddf5d0 100644 --- a/client/components/inline-notice/styles.scss +++ b/client/components/inline-notice/styles.scss @@ -105,4 +105,12 @@ color: $wp-green-70; } } + + // Special case for link variant button. + button.wcpay-inline-notice__action { + &.is-link { + text-decoration: none; // Remove underline. + box-shadow: none; // Remove box shadow. + } + } } diff --git a/client/connect-account-page/style.scss b/client/connect-account-page/style.scss index 2e0b5419dde..0ffe38a59c7 100644 --- a/client/connect-account-page/style.scss +++ b/client/connect-account-page/style.scss @@ -217,6 +217,12 @@ button { @include wp-subtitle; padding: $gap $gap-largest $gap $gap-large !important; + &:focus { + box-shadow: none !important; + } + &:hover { + background: #fff !important; + } } } .components-notice { diff --git a/client/globals.d.ts b/client/globals.d.ts index 601ad52188f..cb3cb7d9a8d 100644 --- a/client/globals.d.ts +++ b/client/globals.d.ts @@ -1,11 +1,7 @@ /** * Internal dependencies */ -import type { - MccsDisplayTreeItem, - Country, - OnboardingFields, -} from 'onboarding/types'; +import type { MccsDisplayTreeItem, Country } from 'onboarding/types'; declare global { const wcpaySettings: { @@ -102,10 +98,6 @@ declare global { business_types: Country[]; mccs_display_tree: MccsDisplayTreeItem[]; }; - onboardingFlowState?: { - current_step: string; - data: OnboardingFields; - }; storeCurrency: string; isMultiCurrencyEnabled: string; errorMessage: string; diff --git a/client/onboarding/context.tsx b/client/onboarding/context.tsx index e13f0e7d946..24a3b211e36 100644 --- a/client/onboarding/context.tsx +++ b/client/onboarding/context.tsx @@ -7,13 +7,12 @@ import { isNil, omitBy } from 'lodash'; /** * Internal dependencies */ -import { OnboardingFields, TempData } from './types'; +import { OnboardingFields } from './types'; const useContextValue = ( initialState = {} as OnboardingFields ) => { const [ data, setData ] = useState( initialState ); const [ errors, setErrors ] = useState( {} as OnboardingFields ); const [ touched, setTouched ] = useState( {} as OnboardingFields ); - const [ temp, setTemp ] = useState( {} as TempData ); return { data, @@ -25,9 +24,6 @@ const useContextValue = ( initialState = {} as OnboardingFields ) => { touched, setTouched: ( value: Record< string, boolean > ) => setTouched( ( prev ) => ( { ...prev, ...value } ) ), - temp, - setTemp: ( value: Partial< TempData > ) => - setTemp( ( prev ) => ( { ...prev, ...value } ) ), }; }; diff --git a/client/onboarding/form.tsx b/client/onboarding/form.tsx index d5cb1eeb4ed..d12e1fb860c 100644 --- a/client/onboarding/form.tsx +++ b/client/onboarding/form.tsx @@ -14,8 +14,6 @@ import { ListItem as GroupedSelectItem } from 'components/grouped-select-control import { GroupedSelectField, GroupedSelectFieldProps, - PhoneNumberField, - PhoneNumberFieldProps, SelectField, SelectFieldProps, TextField, @@ -47,7 +45,11 @@ export const OnboardingForm: React.FC = ( { children } ) => { } } > { children } - @@ -90,38 +92,6 @@ export const OnboardingTextField: React.FC< OnboardingTextFieldProps > = ( ); }; -interface OnboardingPhoneNumberFieldProps - extends Partial< PhoneNumberFieldProps > { - name: keyof OnboardingFields; -} - -export const OnboardingPhoneNumberField: React.FC< OnboardingPhoneNumberFieldProps > = ( - props -) => { - const { name } = props; - const { data, setData, temp, setTemp, touched } = useOnboardingContext(); - const { validate, error } = useValidation( name ); - - return ( - { - setTemp( { phoneCountryCode } ); - setData( { [ name ]: value } ); - if ( touched[ name ] ) validate( value ); - } } - onBlur={ () => validate() } - error={ error() } - onKeyDown={ ( event: React.KeyboardEvent< HTMLInputElement > ) => { - if ( event.key === 'Enter' ) validate(); - } } - { ...props } - /> - ); -}; - interface OnboardingSelectFieldProps< ItemType > extends Partial< Omit< SelectFieldProps< ItemType >, 'onChange' > > { name: keyof OnboardingFields; @@ -143,7 +113,8 @@ export const OnboardingSelectField = < ItemType extends SelectItem >( { ( item ) => item.key === data[ name ] ) } placeholder={ - ( strings.placeholders as Record< string, string > )[ name ] + ( strings.placeholders as Record< string, string > )[ name ] ?? + strings.placeholders.generic } onChange={ ( { selectedItem } ) => { if ( onChange ) { @@ -183,7 +154,8 @@ export const OnboardingGroupedSelectField = < ( item ) => item.key === data[ name ] ) } placeholder={ - ( strings.placeholders as Record< string, string > )[ name ] + ( strings.placeholders as Record< string, string > )[ name ] ?? + strings.placeholders.generic } onChange={ ( { selectedItem } ) => { if ( onChange ) { diff --git a/client/onboarding/index.tsx b/client/onboarding/index.tsx index 58af5238260..42d88da0dbf 100644 --- a/client/onboarding/index.tsx +++ b/client/onboarding/index.tsx @@ -7,21 +7,17 @@ import React, { useEffect } from 'react'; * Internal dependencies */ import Page from 'components/page'; -import { OnboardingContextProvider, useOnboardingContext } from './context'; +import { OnboardingContextProvider } from './context'; import { Stepper } from 'components/stepper'; import { OnboardingForm } from './form'; import Step from './step'; -import PersonalDetails from './steps/personal-details'; import BusinessDetails from './steps/business-details'; import StoreDetails from './steps/store-details'; import LoadingStep from './steps/loading'; import { trackStarted } from './tracking'; import './style.scss'; -import { persistFlowState } from './utils'; const OnboardingStepper = () => { - const { data } = useOnboardingContext(); - const handleExit = () => { if ( window.history.length > 1 && @@ -31,35 +27,10 @@ const OnboardingStepper = () => { window.location.href = wcSettings.adminUrl; }; - const handleStepChange = ( step: string ) => { - window.scroll( 0, 0 ); - persistFlowState( step, data ); - }; - - const initialStep = () => { - // since mode step is not part of the stepper anymore, we need to overwrite it - // Remove it in a future version, once enough time has passed that people won't be likely to have mode or personal saved as this value. - const currentStep = wcpaySettings.onboardingFlowState?.current_step; - if ( - currentStep && - ( currentStep === 'mode' || currentStep === 'personal' ) - ) { - return 'business'; - } - return currentStep; - }; + const handleStepChange = () => window.scroll( 0, 0 ); return ( - - - - - - + @@ -75,7 +46,7 @@ const OnboardingStepper = () => { ); }; -const initialData = wcpaySettings.onboardingFlowState?.data ?? { +const initialData = { business_name: wcSettings?.siteTitle, url: location.hostname === 'localhost' diff --git a/client/onboarding/restored-state-banner.tsx b/client/onboarding/restored-state-banner.tsx deleted file mode 100644 index 8ffef4fc431..00000000000 --- a/client/onboarding/restored-state-banner.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; - -/** - * Internal dependencies - */ -import strings from './strings'; -import BannerNotice from 'components/banner-notice'; - -const RestoredStateBanner: React.FC = () => { - const [ hidden, setHidden ] = React.useState( false ); - if ( hidden || ! wcpaySettings.onboardingFlowState ) return null; - return ( - setHidden( true ) } - > - { strings.restoredState } - - ); -}; - -export default RestoredStateBanner; diff --git a/client/onboarding/step.tsx b/client/onboarding/step.tsx index cc9dda8a368..6976f025765 100644 --- a/client/onboarding/step.tsx +++ b/client/onboarding/step.tsx @@ -9,7 +9,6 @@ import ChevronLeft from 'gridicons/dist/chevron-left'; * Internal dependencies */ import { useStepperContext } from 'components/stepper'; -import RestoredStateBanner from './restored-state-banner'; import { OnboardingSteps } from './types'; import { useTrackAbandoned } from './tracking'; import strings from './strings'; @@ -22,9 +21,7 @@ interface Props { const Step: React.FC< Props > = ( { name, children } ) => { const { trackAbandoned } = useTrackAbandoned(); - const { progress, prevStep, exit } = useStepperContext(); - const width = `${ progress * 100 }%`; - + const { prevStep, exit } = useStepperContext(); const handleExit = () => { trackAbandoned( 'exit' ); exit(); @@ -32,7 +29,6 @@ const Step: React.FC< Props > = ( { name, children } ) => { return ( <> -
-

{ strings.steps[ name ].heading }

diff --git a/client/onboarding/steps/business-details.tsx b/client/onboarding/steps/business-details.tsx index 8a9a3b762bb..42a7ad5bbb2 100644 --- a/client/onboarding/steps/business-details.tsx +++ b/client/onboarding/steps/business-details.tsx @@ -9,22 +9,21 @@ import React from 'react'; import { useOnboardingContext } from '../context'; import { Item } from 'components/custom-select-control'; import { OnboardingFields } from '../types'; -import { - OnboardingGroupedSelectField, - OnboardingSelectField, - OnboardingTextField, -} from '../form'; +import { OnboardingGroupedSelectField, OnboardingSelectField } from '../form'; import { getAvailableCountries, getBusinessTypes, getMccsFlatList, } from 'onboarding/utils'; import { BusinessType } from 'onboarding/types'; +import InlineNotice from 'components/inline-notice'; +import strings from 'onboarding/strings'; const BusinessDetails: React.FC = () => { const { data, setData } = useOnboardingContext(); const countries = getAvailableCountries(); const businessTypes = getBusinessTypes(); + const mccsFlatList = getMccsFlatList(); const selectedCountry = businessTypes.find( ( country ) => country.key === data.country @@ -33,6 +32,14 @@ const BusinessDetails: React.FC = () => { ( type ) => type.key === data.business_type ); + const selectedBusinessStructure = + selectedBusinessType?.structures.length === 0 || + selectedBusinessType?.structures.find( + ( structure ) => structure.key === data[ 'company.structure' ] + ); + + const selectedMcc = mccsFlatList.find( ( mcc ) => mcc.key === data.mcc ); + const handleTiedChange = ( name: keyof OnboardingFields, selectedItem?: Item | null @@ -48,47 +55,83 @@ const BusinessDetails: React.FC = () => { setData( newData ); }; - const mccsFlatList = getMccsFlatList(); - return ( <> - - - - { selectedCountry && selectedCountry.types.length > 0 && ( - handleTiedChange( 'country', null ), + }, + ] } + status="info" > - { ( item: Item & BusinessType ) => ( -
-
{ item.name }
-
- { item.description } -
-
- ) } -
+
+ { strings.inlineNotice.title }{ ' ' } + { selectedCountry.name } +
+ ) } - { selectedBusinessType && - selectedBusinessType.structures.length > 0 && ( + { ! selectedCountry && ( + + + ) } + { selectedCountry && selectedCountry.types.length > 0 && ( + + + { ( item: Item & BusinessType ) => ( +
+
{ item.name }
+
+ { item.description } +
+
+ ) } +
+
+ ) } + { selectedBusinessType && + selectedBusinessType.structures.length > 0 && ( + + + + ) } + { selectedCountry && + selectedBusinessType && + selectedBusinessStructure && ( + + + ) } - + { selectedCountry && + selectedBusinessType && + selectedBusinessStructure && + selectedMcc && ( + + { strings.tos } + + ) } ); }; diff --git a/client/onboarding/steps/loading.tsx b/client/onboarding/steps/loading.tsx index 232d609bf99..feb8053438d 100644 --- a/client/onboarding/steps/loading.tsx +++ b/client/onboarding/steps/loading.tsx @@ -9,7 +9,7 @@ import apiFetch from '@wordpress/api-fetch'; * Internal dependencies */ import { useOnboardingContext } from '../context'; -import { POEligibleData, POEligibleResult } from '../types'; +import { PoEligibleData, PoEligibleResult } from '../types'; import { fromDotNotation } from '../utils'; import { trackRedirected, useTrackAbandoned } from '../tracking'; import LoadBar from 'components/load-bar'; @@ -34,7 +34,7 @@ const LoadingStep: React.FC< Props > = () => { ) { return false; } - const eligibilityDetails: POEligibleData = { + const eligibilityDetails: PoEligibleData = { business: { country: data.country, type: data.business_type, @@ -45,7 +45,7 @@ const LoadingStep: React.FC< Props > = () => { go_live_timeframe: data.go_live_timeframe, }, }; - const eligibleResult = await apiFetch< POEligibleResult >( { + const eligibleResult = await apiFetch< PoEligibleResult >( { path: '/wc/v3/payments/onboarding/router/po_eligible', method: 'POST', data: eligibilityDetails, diff --git a/client/onboarding/steps/personal-details.tsx b/client/onboarding/steps/personal-details.tsx deleted file mode 100644 index 4941ca9df29..00000000000 --- a/client/onboarding/steps/personal-details.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { Flex, FlexBlock } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import strings from '../strings'; -import { OnboardingTextField, OnboardingPhoneNumberField } from '../form'; -import InlineNotice from 'components/inline-notice'; - -const PersonalDetails: React.FC = () => { - return ( - <> - - - - - - - - - - - - { strings.steps.personal.notice } - - - ); -}; - -export default PersonalDetails; diff --git a/client/onboarding/steps/test/business-details.tsx b/client/onboarding/steps/test/business-details.tsx index 66e889f60c7..2bfe392b532 100644 --- a/client/onboarding/steps/test/business-details.tsx +++ b/client/onboarding/steps/test/business-details.tsx @@ -172,58 +172,61 @@ const mccsFlatList = [ mocked( getMccsFlatList ).mockReturnValue( mccsFlatList ); describe( 'BusinessDetails', () => { - it( 'renders and updates fields data when they are changed', () => { + it( 'renders and updates fields data when they are changed', async () => { render( ); - const businessNameField = screen.getByLabelText( - strings.fields.business_name - ); - const urlField = screen.getByLabelText( strings.fields.url ); - const countryField = screen.getByText( strings.placeholders.country ); + const countryField = screen + .getByTestId( 'country-select' ) + .querySelector( 'button' ); - user.type( businessNameField, 'John Doe LLC' ); - user.type( urlField, 'https://johndoe.com' ); + if ( ! countryField ) { + throw new Error( 'Country select not found' ); + } - expect( - screen.queryByText( strings.placeholders.business_type ) - ).not.toBeInTheDocument(); - expect( - screen.queryByText( strings.placeholders[ 'company.structure' ] ) - ).not.toBeInTheDocument(); + expect( countryField ).toBeInTheDocument(); user.click( countryField ); - user.click( screen.getByText( 'Spain' ) ); + await screen.findByText( 'United States' ); + user.click( screen.getByText( 'United States' ) ); - expect( - screen.queryByText( strings.placeholders.business_type ) - ).not.toBeInTheDocument(); + const businessTypeField = screen + .getByTestId( 'business-type-select' ) + .querySelector( 'button' ); - user.click( countryField ); - user.click( screen.getByText( 'United States' ) ); + if ( ! businessTypeField ) { + throw new Error( 'Business type select not found' ); + } - const businessTypeField = screen.getByText( - strings.placeholders.business_type - ); user.click( businessTypeField ); + await screen.findByText( 'Company' ); user.click( screen.getByText( 'Company' ) ); - const companyStructureField = screen.getByText( - strings.placeholders[ 'company.structure' ] - ); + const companyStructureField = screen + .getByTestId( 'business-structure-select' ) + .querySelector( 'button' ); + + if ( ! companyStructureField ) { + throw new Error( 'Company structure select not found' ); + } user.click( companyStructureField ); + await screen.findByText( 'Single member LLC' ); user.click( screen.getByText( 'Single member LLC' ) ); - const mccField = screen.getByText( strings.placeholders.mcc ); + const mccField = screen + .getByTestId( 'mcc-select' ) + .querySelector( 'button' ); + if ( ! mccField ) { + throw new Error( 'MCC select not found' ); + } + user.click( mccField ); + await screen.findByText( 'Popular Software' ); user.click( screen.getByText( 'Popular Software' ) ); - expect( businessNameField ).toHaveValue( 'John Doe LLC' ); - expect( urlField ).toHaveValue( 'https://johndoe.com' ); - expect( countryField ).toHaveTextContent( 'United States' ); expect( businessTypeField ).toHaveTextContent( 'Company' ); expect( companyStructureField ).toHaveTextContent( 'Single member LLC' diff --git a/client/onboarding/steps/test/personal-details.tsx b/client/onboarding/steps/test/personal-details.tsx deleted file mode 100644 index 603cc8192c2..00000000000 --- a/client/onboarding/steps/test/personal-details.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import user from '@testing-library/user-event'; - -/** - * Internal dependencies - */ -import PersonalDetails from '../personal-details'; -import { OnboardingContextProvider } from '../../context'; -import strings from '../../strings'; - -declare const global: { - wcpaySettings: { - connect: { country: string }; - }; -}; - -describe( 'PersonalDetails', () => { - it( 'renders and updates fields data when they are changed', () => { - global.wcpaySettings = { - connect: { country: 'US' }, - }; - - render( - - - - ); - const firstNameField = screen.getByLabelText( - strings.fields[ 'individual.first_name' ] - ); - const lastNameField = screen.getByLabelText( - strings.fields[ 'individual.last_name' ] - ); - const emailField = screen.getByLabelText( strings.fields.email ); - const phoneField = screen.getByLabelText( strings.fields.phone ); - - user.type( firstNameField, 'John' ); - user.type( lastNameField, 'Doe' ); - user.type( emailField, 'john@doe.com' ); - user.type( phoneField, '000000000' ); - - expect( firstNameField ).toHaveValue( 'John' ); - expect( lastNameField ).toHaveValue( 'Doe' ); - expect( emailField ).toHaveValue( 'john@doe.com' ); - expect( phoneField ).toHaveValue( '000000000' ); - } ); -} ); diff --git a/client/onboarding/strings.tsx b/client/onboarding/strings.tsx index 69bd69461e9..02ea32d2998 100644 --- a/client/onboarding/strings.tsx +++ b/client/onboarding/strings.tsx @@ -14,82 +14,9 @@ const documentationUrls = { export default { steps: { - mode: { - heading: __( - 'Let’s get your store ready to accept payments', - 'woocommerce-payments' - ), - subheading: __( - 'Select the option that best fits your needs.', - 'woocommerce-payments' - ), - label: __( - 'I’d like to set up payments for my store', - 'woocommerce-payments' - ), - note: __( - 'You’ll need to provide details to verify that you’re the owner of the account. If you’re setting up payments for someone else, choose sandbox mode.', - 'woocommerce-payments' - ), - continue: { - live: __( 'Continue', 'woocommerce-payments' ), - test: __( 'Continue in sandbox mode', 'woocommerce-payments' ), - }, - tos: interpolateComponents( { - mixedString: sprintf( - __( - /* translators: %1$s: WooPayments, %2$s: WooPay */ - 'By using %1$s, you agree to the {{tosLink}}Terms of Service{{/tosLink}} (including %2$s {{merchantTermsLink}}merchant terms{{/merchantTermsLink}}) and acknowledge that you have read our {{privacyPolicyLink}}Privacy Policy{{/privacyPolicyLink}}.', - 'woocommerce-payments' - ), - 'WooPayments', - 'WooPay' - ), - components: { - tosLink: ( - // eslint-disable-next-line jsx-a11y/anchor-has-content - - ), - merchantTermsLink: ( - // eslint-disable-next-line jsx-a11y/anchor-has-content - - ), - privacyPolicyLink: ( - // eslint-disable-next-line jsx-a11y/anchor-has-content - - ), - }, - } ), - }, - personal: { - heading: __( - 'First, you’ll need to create an account', - 'woocommerce-payments' - ), - subheading: __( - 'The information below should reflect that of the business owner or an authorized team member.', - 'woocommerce-payments' - ), - notice: __( - 'We’ll use the email address to contact you with any important notifications related to your account, and the phone number will only be used to protect your account with two-factor authentication.', - 'woocommerce-payments' - ), - }, business: { heading: __( - 'Tell us about your business', + 'Let’s get your store ready to accept payments', 'woocommerce-payments' ), subheading: __( @@ -109,25 +36,16 @@ export default { }, loading: { heading: __( - 'Let’s get you set up for payments', + 'One last step! Verify your identity with our partner', 'woocommerce-payments' ), subheading: __( - 'Confirm your identity with our partner', + 'This will take place in a secure environment through our partner. Once your business details are verified, you’ll be redirected back to your store dashboard.', 'woocommerce-payments' ), }, }, fields: { - email: __( 'What’s your email address?', 'woocommerce-payments' ), - 'individual.first_name': __( 'First name', 'woocommerce-payments' ), - 'individual.last_name': __( 'Last name', 'woocommerce-payments' ), - phone: __( 'What’s your mobile phone number?', 'woocommerce-payments' ), - business_name: __( - 'What’s the legal name of your business?', - 'woocommerce-payments' - ), - url: __( 'What’s your business website?', 'woocommerce-payments' ), country: __( 'Where is your business legally registered?', 'woocommerce-payments' @@ -155,24 +73,6 @@ export default { }, errors: { generic: __( 'Please provide a response', 'woocommerce-payments' ), - 'individual.first_name': __( - 'Please provide a first name', - 'woocommerce-payments' - ), - 'individual.last_name': __( - 'Please provide a last name', - 'woocommerce-payments' - ), - email: __( 'Please provide a valid email', 'woocommerce-payments' ), - phone: __( - 'Please provide a valid phone number', - 'woocommerce-payments' - ), - url: __( 'Please provide a valid website', 'woocommerce-payments' ), - business_name: __( - 'Please provide a business name', - 'woocommerce-payments' - ), country: __( 'Please provide a country', 'woocommerce-payments' ), business_type: __( 'Please provide a business type', @@ -184,22 +84,8 @@ export default { ), }, placeholders: { - country: __( - 'Select the primary country of your business', - 'woocommerce-payments' - ), - business_type: __( - 'Select the legal structure of your business', - 'woocommerce-payments' - ), - 'company.structure': __( - 'Select the legal category of your business', - 'woocommerce-payments' - ), - mcc: __( - 'Select the primary industry of your business', - 'woocommerce-payments' - ), + generic: __( 'Select an option', 'woocommerce-payments' ), + country: __( 'Select a country', 'woocommerce-payments' ), annual_revenue: __( 'Select your annual revenue', 'woocommerce-payments' @@ -220,10 +106,47 @@ export default { from_3_to_6months: __( '3 – 6 months', 'woocommerce-payments' ), more_than_6months: __( '6+ months', 'woocommerce-payments' ), }, - restoredState: __( - 'We have restored your previous session. You can pick up where you left off, or go back to a previous step to make changes. ', - 'woocommerce-payments' - ), + tos: interpolateComponents( { + mixedString: sprintf( + __( + /* translators: %1$s: WooPayments, %2$s: WooPay */ + 'By using %1$s, you agree to be bound by our {{tosLink}}Terms of Service{{/tosLink}} (including {{merchantTermsLink}}%2$s merchant terms{{/merchantTermsLink}}) and acknowledge that you have read our {{privacyPolicyLink}}Privacy Policy{{/privacyPolicyLink}}.', + 'woocommerce-payments' + ), + 'WooPayments', + 'WooPay' + ), + components: { + tosLink: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + merchantTermsLink: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + privacyPolicyLink: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ), + inlineNotice: { + title: __( 'Business Location:', 'woocommerce-payments' ), + action: __( 'Change', 'woocommerce-payments' ), + }, continue: __( 'Continue', 'woocommerce-payments' ), back: __( 'Back', 'woocommerce-payments' ), }; diff --git a/client/onboarding/style.scss b/client/onboarding/style.scss index 8dbbef819e4..9408cd1e1ef 100644 --- a/client/onboarding/style.scss +++ b/client/onboarding/style.scss @@ -7,16 +7,6 @@ body.wcpay-onboarding__body { } .stepper { - &__progress { - position: fixed; - top: 0; - left: 0; - height: 8px; - background-color: var( --wp-admin-theme-color ); - z-index: 11; - transition: width 250ms; - } - &__nav { position: fixed; top: 0; @@ -56,8 +46,8 @@ body.wcpay-onboarding__body { } &__wrapper { - max-width: 600px; - margin: 88px auto 0; + max-width: 620px; + margin: 116px auto 0; display: flex; flex-direction: column; align-items: center; @@ -65,6 +55,7 @@ body.wcpay-onboarding__body { &__heading { @include wp-title-large; + font-family: 'SF Pro Display', $default-font; color: $studio-gray-100; text-align: center; } @@ -89,10 +80,41 @@ body.wcpay-onboarding__body { &__cta { display: block; width: 100%; - margin-top: $gap-larger; + height: 40px; // Matching the updated WP Component. We can remove this when we update Components version. + margin-top: $gap-large; + } + } + + .wcpay-inline-notice { + background-color: $gray-0; + &__content { + display: flex; + width: 100%; + &__title { + width: inherit; + } + &__actions { + width: auto; + padding-top: 0; + } } } + .wcpay-onboarding__tos { + font-size: 12px; + } + + .wcpay-component-grouped-select-control__button-value { + color: $gray-900; + } + + .complete-business-info-task__option-description { + font-size: 12px; + color: $gray-700; + line-height: 16px; + margin-top: 4px; + } + .loading-step { max-width: 520px; position: absolute; @@ -125,10 +147,6 @@ body.wcpay-onboarding__body { .components-form-field__error { margin: -$gap 0 $gap; } - - .restored-state-banner { - margin: $gap 0; - } } .wcpay-component-onboarding-card { diff --git a/client/onboarding/test/context.tsx b/client/onboarding/test/context.tsx index 3344cb1aa72..a9ffe225dea 100644 --- a/client/onboarding/test/context.tsx +++ b/client/onboarding/test/context.tsx @@ -23,13 +23,13 @@ describe( 'OnboardingContext', () => { } = useOnboardingContext(); const handleClick = () => { setData( { - url: 'URL', + business_type: 'Individual', } ); setErrors( { - url: 'Required', + business_type: 'Required', } ); setTouched( { - url: true, + business_type: true, } ); }; return ( @@ -42,7 +42,7 @@ describe( 'OnboardingContext', () => { ); }; - const initialData = { url: 'Initial' }; + const initialData = { business_type: 'Individual' }; render( @@ -51,19 +51,21 @@ describe( 'OnboardingContext', () => { ); expect( - screen.getByText( 'data: {"url":"Initial"}' ) + screen.getByText( 'data: {"business_type":"Individual"}' ) ).toBeInTheDocument(); expect( screen.getByText( 'errors: {}' ) ).toBeInTheDocument(); expect( screen.getByText( 'touched: {}' ) ).toBeInTheDocument(); user.click( screen.getByText( 'Update Data' ) ); - expect( screen.getByText( 'data: {"url":"URL"}' ) ).toBeInTheDocument(); expect( - screen.getByText( 'errors: {"url":"Required"}' ) + screen.getByText( 'data: {"business_type":"Individual"}' ) ).toBeInTheDocument(); expect( - screen.getByText( 'touched: {"url":true}' ) + screen.getByText( 'errors: {"business_type":"Required"}' ) + ).toBeInTheDocument(); + expect( + screen.getByText( 'touched: {"business_type":true}' ) ).toBeInTheDocument(); } ); diff --git a/client/onboarding/test/form.tsx b/client/onboarding/test/form.tsx index d80b6601025..91abec821d6 100644 --- a/client/onboarding/test/form.tsx +++ b/client/onboarding/test/form.tsx @@ -12,7 +12,6 @@ import { OnboardingForm, OnboardingTextField, OnboardingSelectField, - OnboardingPhoneNumberField, } from '../form'; declare const global: { @@ -114,55 +113,65 @@ describe( 'Onboarding Form', () => { describe( 'OnboardingTextField', () => { it( 'renders component with provided props ', () => { - data = { 'individual.first_name': 'John' }; + data = { annual_revenue: 'Less than $250k' }; error.mockReturnValue( 'error message' ); - render( ); + render( ); - const textField = screen.getByLabelText( 'First name' ); + const textField = screen.getByLabelText( + 'What is your estimated annual Ecommerce revenue (USD)?' + ); const errorMessage = screen.getByText( 'error message' ); - expect( textField ).toHaveValue( 'John' ); + expect( textField ).toHaveValue( 'Less than $250k' ); expect( errorMessage ).toBeInTheDocument(); } ); it( 'calls setData on change', () => { - render( ); + render( ); - const textField = screen.getByLabelText( 'First name' ); + const textField = screen.getByLabelText( + 'What is your estimated annual Ecommerce revenue (USD)?' + ); textField.focus(); // Workaround for `type` not triggering focus. - userEvent.type( textField, 'John' ); + userEvent.type( textField, 'Less than $250k' ); expect( setData ).toHaveBeenCalledWith( { - 'individual.first_name': 'John', + annual_revenue: 'Less than $250k', } ); expect( validate ).not.toHaveBeenCalled(); } ); it( 'calls validate on change if touched', () => { - touched = { 'individual.first_name': true }; - render( ); + touched = { annual_revenue: true }; + render( ); - const textField = screen.getByLabelText( 'First name' ); + const textField = screen.getByLabelText( + 'What is your estimated annual Ecommerce revenue (USD)?' + ); userEvent.type( textField, 'John' ); expect( validate ).toHaveBeenCalledWith( 'John' ); } ); it( 'calls validate on change if not focused', () => { - render( ); + render( ); - const textField = screen.getByLabelText( 'First name' ); + const textField = screen.getByLabelText( + 'What is your estimated annual Ecommerce revenue (USD)?' + ); userEvent.type( textField, 'John' ); expect( validate ).toHaveBeenCalledWith( 'John' ); } ); it( 'calls validate on blur', () => { - render( ); + render( ); - const textField = screen.getByLabelText( 'First name' ); + const textField = screen.getByLabelText( + 'What is your estimated annual Ecommerce revenue (USD)?' + ); userEvent.type( textField, 'John' ); userEvent.tab(); fireEvent.focusOut( textField ); // Workaround for onFocus event not firing with jsdom <16.3.0 @@ -208,64 +217,4 @@ describe( 'Onboarding Form', () => { expect( validate ).toHaveBeenCalledWith( 'individual' ); } ); } ); - - describe( 'OnboardingPhoneNumberField', () => { - it( 'renders component with provided props ', () => { - data = { phone: '+123' }; - error.mockReturnValue( 'error message' ); - - render( ); - - const textField = screen.getByLabelText( - 'What’s your mobile phone number?' - ); - const errorMessage = screen.getByText( 'error message' ); - - expect( textField ).toHaveValue( '23' ); - expect( errorMessage ).toBeInTheDocument(); - } ); - - it( 'calls setTemp and setData on change', () => { - render( ); - - const textField = screen.getByLabelText( - 'What’s your mobile phone number?' - ); - userEvent.type( textField, '23' ); - - expect( setTemp ).toHaveBeenCalledWith( { - phoneCountryCode: 'US', - } ); - - expect( setData ).toHaveBeenCalledWith( { - phone: '+123', - } ); - expect( validate ).not.toHaveBeenCalledWith(); - } ); - - it( 'only calls validate on change if touched', () => { - touched = { phone: true }; - render( ); - - const textField = screen.getByLabelText( - 'What’s your mobile phone number?' - ); - userEvent.type( textField, '23' ); - - expect( validate ).toHaveBeenCalledWith( '+123' ); - } ); - - it( 'calls validate on blur', () => { - render( ); - - const textField = screen.getByLabelText( - 'What’s your mobile phone number?' - ); - userEvent.type( textField, '23' ); - userEvent.tab(); - fireEvent.focusOut( textField ); // Workaround for onFocus event not firing with jsdom <16.3.0 - - expect( validate ).toHaveBeenCalledWith(); - } ); - } ); } ); diff --git a/client/onboarding/test/validation.ts b/client/onboarding/test/validation.ts index f5d167e11da..5bf58cdb02e 100644 --- a/client/onboarding/test/validation.ts +++ b/client/onboarding/test/validation.ts @@ -10,28 +10,6 @@ import { useValidation } from '../validation'; import { OnboardingContextProvider } from '../context'; describe( 'useValidation', () => { - it( 'sets email error state for an invalid value', () => { - const { result } = renderHook( () => useValidation( 'email' ), { - wrapper: OnboardingContextProvider, - } ); - - act( () => result.current.validate( 'invalid' ) ); - - expect( result.current.error() ).toEqual( - 'Please provide a valid email' - ); - } ); - - it( 'sets email error state to undefined for a valid value', () => { - const { result } = renderHook( () => useValidation( 'email' ), { - wrapper: OnboardingContextProvider, - } ); - - act( () => result.current.validate( 'valid@email.com' ) ); - - expect( result.current.error() ).toBeUndefined(); - } ); - it( 'uses a generic string for a non existing error', () => { const { result } = renderHook( () => useValidation( 'annual_revenue' ), diff --git a/client/onboarding/types.ts b/client/onboarding/types.ts index 0f945551670..2f57aa08e94 100644 --- a/client/onboarding/types.ts +++ b/client/onboarding/types.ts @@ -2,15 +2,9 @@ * Internal dependencies */ -export type OnboardingSteps = 'personal' | 'business' | 'store' | 'loading'; +export type OnboardingSteps = 'business' | 'store' | 'loading'; export type OnboardingFields = { - email?: string; - 'individual.first_name'?: string; - 'individual.last_name'?: string; - phone?: string; - business_name?: string; - url?: string; country?: string; business_type?: string; 'company.structure'?: string; @@ -19,11 +13,11 @@ export type OnboardingFields = { go_live_timeframe?: string; }; -export interface POEligibleResult { +export interface PoEligibleResult { result: 'eligible' | 'not_eligible'; } -export interface POEligibleData { +export interface PoEligibleData { business: { country: string; type: string; @@ -35,10 +29,6 @@ export interface POEligibleData { }; } -export type TempData = { - phoneCountryCode?: string; -}; - export interface Country { key: string; name: string; diff --git a/client/onboarding/utils.ts b/client/onboarding/utils.ts index 4e5f392fb59..c45c218f9b0 100644 --- a/client/onboarding/utils.ts +++ b/client/onboarding/utils.ts @@ -2,15 +2,13 @@ * External dependencies */ import { set, toPairs } from 'lodash'; -import apiFetch from '@wordpress/api-fetch'; /** * Internal dependencies */ -import { NAMESPACE } from 'data/constants'; import { ListItem } from 'components/grouped-select-control'; import businessTypeDescriptionStrings from './translations/descriptions'; -import { Country, OnboardingFields } from './types'; +import { Country } from './types'; export const fromDotNotation = ( record: Record< string, unknown > @@ -86,14 +84,3 @@ export const getMccsFlatList = (): ListItem[] => { ]; }, [] as ListItem[] ); }; - -export const persistFlowState = ( - currentStep: string, - data: OnboardingFields -): Promise< void > => - apiFetch( { - path: `${ NAMESPACE }/onboarding/flow-state`, - method: 'POST', - data: { current_step: currentStep, data }, - parse: false, - } ); diff --git a/client/onboarding/validation.ts b/client/onboarding/validation.ts index d858b83e3d5..b55694dc1e6 100644 --- a/client/onboarding/validation.ts +++ b/client/onboarding/validation.ts @@ -14,10 +14,6 @@ const isValid = ( name: keyof OnboardingFields, value?: string ): boolean => { if ( ! value ) return false; switch ( name ) { - case 'email': - return value.includes( '@' ); - case 'phone': - return /^\+\d{7,}$/.test( value ); default: return true; } diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 9cb0e033a1b..d82c14f0cf5 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -805,7 +805,6 @@ 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() ), - 'onboardingFlowState' => $this->onboarding_service->get_onboarding_flow_state(), '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 b8eab24046d..74cbac4dc9f 100644 --- a/includes/admin/class-wc-rest-payments-onboarding-controller.php +++ b/includes/admin/class-wc-rest-payments-onboarding-controller.php @@ -124,28 +124,6 @@ public function register_routes() { 'permission_callback' => [ $this, 'check_permission' ], ], ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/flow-state', - [ - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'update_flow_state' ], - 'permission_callback' => [ $this, 'check_permission' ], - 'args' => [ - 'current_step' => [ - 'required' => true, - 'description' => 'The current step of the onboarding process.', - 'type' => 'string', - ], - 'data' => [ - 'required' => true, - 'description' => 'The onboarding context data.', - 'type' => 'object', - ], - ], - ], - ); } /** @@ -210,14 +188,4 @@ public function get_progressive_onboarding_eligible( WP_REST_Request $request ) ] ); } - - /** - * Update the onboarding flow state. - * - * @param WP_REST_Request $request Request object. - * @return void - */ - public function update_flow_state( WP_REST_Request $request ) { - $this->onboarding_service->set_onboarding_flow_state( $request->get_json_params() ); - } } diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 757f9b34018..e4e07883fed 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -1440,9 +1440,6 @@ private function init_stripe_onboarding( $wcpay_connect_from, $additional_args = WC_Payments_Onboarding_Service::set_test_mode( true ); } - // Clear persisted onboarding flow state. - WC_Payments_Onboarding_Service::clear_onboarding_flow_state(); - if ( ! $collect_payout_requirements ) { // Clear onboarding related account options if this is an initial onboarding attempt. WC_Payments_Onboarding_Service::clear_account_options(); diff --git a/includes/class-wc-payments-onboarding-service.php b/includes/class-wc-payments-onboarding-service.php index 71453b2dda4..2fd28026802 100644 --- a/includes/class-wc-payments-onboarding-service.php +++ b/includes/class-wc-payments-onboarding-service.php @@ -18,7 +18,6 @@ class WC_Payments_Onboarding_Service { const TEST_MODE_OPTION = 'wcpay_onboarding_test_mode'; - const ONBOARDING_FLOW_STATE_OPTION = 'wcpay_onboarding_flow_state'; const ONBOARDING_ELIGIBILITY_MODAL_OPTION = 'wcpay_onboarding_eligibility_modal_dismissed'; const SOURCE_WCADMIN_PAYMENT_TASK = 'wcadmin-payment-task'; const SOURCE_WCADMIN_SETTINGS_PAGE = 'wcadmin-settings-page'; @@ -206,34 +205,6 @@ public function add_admin_body_classes( string $classes = '' ): string { return $classes; } - /** - * Get the onboarding flow state. - * - * @return ?array The onboarding flow state, or null if not set. - */ - public function get_onboarding_flow_state(): ?array { - return get_option( self::ONBOARDING_FLOW_STATE_OPTION, null ); - } - - /** - * Set the onboarding flow state. - * - * @param array $value The onboarding flow state. - * @return bool Whether the option was updated successfully. - */ - public function set_onboarding_flow_state( array $value ): bool { - return update_option( self::ONBOARDING_FLOW_STATE_OPTION, $value ); - } - - /** - * Clear the onboarding flow state. - * - * @return boolean Whether the option was deleted successfully. - */ - public static function clear_onboarding_flow_state(): bool { - return delete_option( self::ONBOARDING_FLOW_STATE_OPTION ); - } - /** * 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/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php b/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php index ee220f5b43c..06606c14902 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 @@ -193,23 +193,4 @@ public function test_get_progressive_onboarding_not_eligible() { $response->get_data() ); } - - public function test_update_flow_state() { - $state = [ - 'current_step' => 'personal', - 'data' => [], - ]; - - $request = new WP_REST_Request( 'POST' ); - $request->set_header( 'Content-Type', 'application/json' ); - $request->set_body( wp_json_encode( $state ) ); - - $this->mock_onboarding_service - ->expects( $this->once() ) - ->method( 'set_onboarding_flow_state' ) - ->with( $state ) - ->willReturn( true ); - - $this->controller->update_flow_state( $request ); - } } diff --git a/tests/unit/test-class-wc-payments-onboarding-service.php b/tests/unit/test-class-wc-payments-onboarding-service.php index 4cdc5596842..7f32038c975 100644 --- a/tests/unit/test-class-wc-payments-onboarding-service.php +++ b/tests/unit/test-class-wc-payments-onboarding-service.php @@ -220,26 +220,6 @@ public function test_set_test_mode() { delete_option( 'wcpay_onboarding_test_mode' ); } - public function test_get_onboarding_flow_state() { - $this->assertNull( $this->onboarding_service->get_onboarding_flow_state() ); - - update_option( WC_Payments_Onboarding_Service::ONBOARDING_FLOW_STATE_OPTION, [] ); - - $this->assertEquals( [], $this->onboarding_service->get_onboarding_flow_state() ); - - delete_option( WC_Payments_Onboarding_Service::ONBOARDING_FLOW_STATE_OPTION ); - } - - public function test_set_onboarding_flow_state() { - $this->assertFalse( get_option( WC_Payments_Onboarding_Service::ONBOARDING_FLOW_STATE_OPTION ) ); - - $this->onboarding_service->set_onboarding_flow_state( [] ); - - $this->assertEquals( [], get_option( WC_Payments_Onboarding_Service::ONBOARDING_FLOW_STATE_OPTION ) ); - - delete_option( WC_Payments_Onboarding_Service::ONBOARDING_FLOW_STATE_OPTION ); - } - /** * @dataProvider data_get_source */