Skip to content

Commit d0de790

Browse files
committed
workable version of campaign budget component
1 parent fe401e4 commit d0de790

28 files changed

+1213
-37
lines changed

.externalized.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
["@woocommerce/components","@woocommerce/currency","@woocommerce/navigation","@woocommerce/number","@woocommerce/settings","@wordpress/api-fetch","@wordpress/components","@wordpress/compose","@wordpress/data","@wordpress/element","@wordpress/hooks","@wordpress/i18n","@wordpress/url","lodash","react","react/jsx-runtime"]
1+
["@woocommerce/components","@woocommerce/currency","@woocommerce/navigation","@woocommerce/number","@woocommerce/settings","@wordpress/api-fetch","@wordpress/components","@wordpress/compose","@wordpress/data","@wordpress/element","@wordpress/hooks","@wordpress/i18n","@wordpress/primitives","@wordpress/url","lodash","react","react/jsx-runtime"]

js/src/components/paid-ads/ads-campaign/index.js

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@ import { __ } from '@wordpress/i18n';
66
/**
77
* Internal dependencies
88
*/
9+
import { useAdaptiveFormContext } from '~/components/adaptive-form';
910
import StepContent from '~/components/stepper/step-content';
1011
import StepContentHeader from '~/components/stepper/step-content-header';
1112
import StepContentFooter from '~/components/stepper/step-content-footer';
1213
import StepContentActions from '~/components/stepper/step-content-actions';
13-
import AppButton from '~/components/app-button';
1414
import PaidAdsFeaturesSection from './paid-ads-feature-section';
15-
import BudgetSetup from '../budget-setup';
1615
import BillingCard from '../billing-card';
16+
import BudgetSection from '../budget-section';
17+
18+
export default function AdsCampaign( {
19+
headerTitle,
20+
skipButton,
21+
continueButton,
22+
} ) {
23+
const formContext = useAdaptiveFormContext();
1724

18-
export default function AdsCampaign( { headerTitle, onSkip, onContinue } ) {
1925
return (
2026
<StepContent>
2127
<StepContentHeader
@@ -26,23 +32,17 @@ export default function AdsCampaign( { headerTitle, onSkip, onContinue } ) {
2632
) }
2733
/>
2834
<PaidAdsFeaturesSection />
29-
<BudgetSetup />
35+
<BudgetSection />
3036
<BillingCard />
3137
<StepContentFooter>
3238
<StepContentActions>
33-
<AppButton
34-
isLink
35-
text={ __(
36-
'Skip ads creation',
37-
'reddit-for-woocommerce'
38-
) }
39-
onClick={ onSkip }
40-
/>
41-
<AppButton
42-
isPrimary
43-
text={ __( 'Continue', 'reddit-for-woocommerce' ) }
44-
onClick={ onContinue }
45-
/>
39+
{ typeof skipButton === 'function'
40+
? skipButton( formContext )
41+
: skipButton }
42+
43+
{ typeof continueButton === 'function'
44+
? continueButton( formContext )
45+
: continueButton }
4646
</StepContentActions>
4747
</StepContentFooter>
4848
</StepContent>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { __ } from '@wordpress/i18n';
5+
import { Tip } from '@wordpress/components';
6+
7+
/**
8+
* Internal dependencies
9+
*/
10+
import Section from '~/components/section';
11+
import Subsection from '~/components/subsection';
12+
import BudgetSetup from '../budget-setup';
13+
import './index.scss';
14+
15+
/**
16+
* Renders a UI for setting up the campaign budget.
17+
*
18+
* @param {Object} props React props.
19+
* @param {JSX.Element} [props.children] Extra content to be rendered under the card of budget inputs.
20+
*/
21+
const BudgetSection = ( { children } ) => {
22+
return (
23+
<div className="rfw-budget-section">
24+
<Section verticalGap={ 4 }>
25+
<Section.Card>
26+
<Section.Card.Body className="rfw-budget-section__card-body">
27+
<div>
28+
<Subsection.Title>
29+
{ __(
30+
'Average daily budget',
31+
'reddit-for-woocommerce'
32+
) }
33+
</Subsection.Title>
34+
<Subsection.Subtitle>
35+
{ __(
36+
'These values are based on your settings and the budgets of similar advertisers.',
37+
'reddit-for-woocommerce'
38+
) }
39+
</Subsection.Subtitle>
40+
</div>
41+
42+
<BudgetSetup />
43+
44+
<Tip>
45+
{ __(
46+
'We recommend running campaigns at least 1 month so it can learn to optimize for your business.',
47+
'reddit-for-woocommerce'
48+
) }
49+
</Tip>
50+
</Section.Card.Body>
51+
</Section.Card>
52+
{ children }
53+
</Section>
54+
</div>
55+
);
56+
};
57+
58+
export default BudgetSection;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.rfw-budget-section {
2+
&__card-body {
3+
display: flex;
4+
flex-direction: column;
5+
gap: var(--main-gap);
6+
}
7+
8+
.rfw-subsection-title {
9+
margin-bottom: wpVariables.$grid-unit-05;
10+
font-weight: 400;
11+
}
12+
13+
.components-tip {
14+
padding: wpVariables.$grid-unit-15 wpVariables.$grid-unit-20;
15+
background-color: #f0f6fc;
16+
17+
> p {
18+
line-height: variables.$rfw-line-height-smaller;
19+
font-size: variables.$rfw-font-smaller;
20+
color: wpColors.$black;
21+
}
22+
23+
> svg {
24+
align-self: initial;
25+
margin: wpVariables.$grid-unit-05 wpVariables.$grid-unit-05 * 2.5 0 0;
26+
fill: wpColors.$gray-900;
27+
}
28+
}
29+
}

js/src/components/paid-ads/budget-setup/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useDebounce } from '@wordpress/compose';
99
/**
1010
* Internal dependencies
1111
*/
12+
import { useAdaptiveFormContext } from '~/components/adaptive-form';
1213
import useAdsCurrency from '~/hooks/useAdsCurrency';
1314
import useBudgetMetrics from '~/hooks/useBudgetMetrics';
1415
import DailyBudgetLabel from './daily-budget-label';
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Internal dependencies
3+
*/
4+
import AdaptiveForm from '~/components/adaptive-form';
5+
import validateCampaign from '~/components/paid-ads/validateCampaign';
6+
import useAdsCurrency from '~/hooks/useAdsCurrency';
7+
8+
/**
9+
* @typedef {import('~/components/types.js').CampaignFormValues} CampaignFormValues
10+
* @typedef {import('~/data/types.js').AssetEntityGroup} AssetEntityGroup
11+
* @typedef {import('~/data/actions').CountryCode} CountryCode
12+
*/
13+
14+
function injectDailyBudget( values, budgetRecommendation ) {
15+
return Object.defineProperty( values, 'dailyBudget', {
16+
enumerable: true,
17+
get() {
18+
if ( this.level === 'custom' ) {
19+
return this.amount;
20+
}
21+
22+
if ( this.level === 'current' ) {
23+
return this.currentAmount;
24+
}
25+
26+
return budgetRecommendation[ this.level ].dailyBudget;
27+
},
28+
} );
29+
}
30+
31+
function resolveInitialCampaign(
32+
initialCampaign,
33+
defaultCampaign,
34+
budgetRecommendation
35+
) {
36+
const values = {
37+
...defaultCampaign,
38+
...initialCampaign,
39+
};
40+
41+
if (
42+
values.level !== 'custom' &&
43+
! budgetRecommendation?.[ values.level ]
44+
) {
45+
values.level =
46+
budgetRecommendation?.recommended && values.level !== 'current'
47+
? 'recommended'
48+
: 'current';
49+
}
50+
51+
return injectDailyBudget( values, budgetRecommendation );
52+
}
53+
54+
/**
55+
* Renders a form based on AdaptiveForm for managing campaign and assets.
56+
*
57+
* @augments AdaptiveForm
58+
* @param {Object} props React props.
59+
* @param {CampaignFormValues} props.initialCampaign Initial campaign values.
60+
* @param {AssetEntityGroup} [props.assetEntityGroup] The asset entity group to be used in initializing the form values for editing.
61+
* @param {Array<CountryCode>} props.countryCodes Country codes to fetch budget recommendations.
62+
* @param {number} [props.currentAmount] Current daily budget amount of the campaign.
63+
*/
64+
export default function CampaignAssetsForm( {
65+
initialCampaign,
66+
assetEntityGroup,
67+
countryCodes,
68+
currentAmount,
69+
...adaptiveFormProps
70+
} ) {
71+
const { formatAmount } = useAdsCurrency();
72+
const budgetRecommendation = {
73+
dailyBudgetBaseline: 13,
74+
recommended: {
75+
dailyBudget: 46.17,
76+
metrics: {
77+
cost: 323.19,
78+
conversions: 2.1,
79+
conversionsValue: 87.41182588215452,
80+
},
81+
country: 'ZW',
82+
currency: 'USD',
83+
},
84+
high: {
85+
dailyBudget: 55.4,
86+
metrics: {
87+
cost: 387.8,
88+
conversions: 2.2,
89+
conversionsValue: 87.41182588215452,
90+
},
91+
country: 'ZW',
92+
currency: 'USD',
93+
},
94+
low: {
95+
dailyBudget: 36.94,
96+
metrics: {
97+
cost: 258.58,
98+
conversions: 2.3,
99+
conversionsValue: 87.41182588215452,
100+
},
101+
country: 'ZW',
102+
currency: 'USD',
103+
},
104+
recommendedDailyBudget: 46.17,
105+
eventProps: {
106+
source: 'reddit-ads-api',
107+
recommended_budget: 46.17,
108+
metrics_availability: 'all',
109+
},
110+
};
111+
112+
const validateCampaignWithMinimumAmount = ( values ) => {
113+
return validateCampaign( values, {
114+
formatAmount,
115+
} );
116+
};
117+
118+
const handleChange = function ( ...args ) {
119+
if ( adaptiveFormProps.onChange ) {
120+
return adaptiveFormProps.onChange.apply( this, args );
121+
}
122+
};
123+
124+
const extendAdapter = () => {
125+
return {
126+
budgetRecommendation,
127+
};
128+
};
129+
130+
return (
131+
<AdaptiveForm
132+
initialValues={ {
133+
...resolveInitialCampaign(
134+
initialCampaign,
135+
{
136+
level: 'recommended',
137+
},
138+
budgetRecommendation
139+
),
140+
} }
141+
validate={ validateCampaignWithMinimumAmount }
142+
extendAdapter={ extendAdapter }
143+
{ ...adaptiveFormProps }
144+
onChange={ handleChange }
145+
/>
146+
);
147+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { __, sprintf } from '@wordpress/i18n';
5+
6+
/**
7+
* @typedef {import('~/components/types.js').CampaignFormValues} CampaignFormValues
8+
*/
9+
10+
/**
11+
* @typedef {Object} ValidateCampaignOptions
12+
* @property {number|undefined} dailyBudget Daily budget for the campaign.
13+
* @property {Function} formatAmount A function to format the budget amount according to the currency settings.
14+
*/
15+
16+
// Minimum percentage of the recommended daily budget.
17+
const BUDGET_MIN_PERCENT = 0.3;
18+
19+
/**
20+
* Validate campaign form. Accepts the form values object and returns errors object.
21+
*
22+
* @param {CampaignFormValues} values Campaign form values.
23+
* @param {ValidateCampaignOptions} opts Extra form options.
24+
* @return {Object} errors.
25+
*/
26+
const validateCampaign = ( values, opts ) => {
27+
const errors = {};
28+
29+
// Only the amount entered by the user needs to be verified.
30+
if ( values.level !== 'custom' ) {
31+
return errors;
32+
}
33+
34+
if (
35+
Number.isFinite( values.amount ) &&
36+
Number.isFinite( opts.dailyBudget ) &&
37+
opts.dailyBudget > 0
38+
) {
39+
const { amount } = values;
40+
const { dailyBudget, formatAmount } = opts;
41+
42+
const minAmount = Math.ceil( dailyBudget * BUDGET_MIN_PERCENT );
43+
44+
if ( amount < minAmount ) {
45+
return {
46+
amount: sprintf(
47+
/* translators: %1$s: minimum daily budget */
48+
__(
49+
'Please make sure daily average cost is at least %s',
50+
'reddit-for-woocommerce'
51+
),
52+
formatAmount( minAmount )
53+
),
54+
};
55+
}
56+
}
57+
58+
if ( ! Number.isFinite( values.amount ) || values.amount <= 0 ) {
59+
return {
60+
amount: __(
61+
'Please make sure daily average cost is greater than 0.',
62+
'reddit-for-woocommerce'
63+
),
64+
};
65+
}
66+
67+
return errors;
68+
};
69+
70+
export default validateCampaign;

js/src/constants.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,8 @@ export const ACCOUNT_TYPE = {
2424
BUSINESS: 'business',
2525
PIXEL: 'pixel',
2626
};
27+
28+
export const GUIDE_NAMES = {
29+
SUBMISSION_SUCCESS: 'submission-success',
30+
CAMPAIGN_CREATION_SUCCESS: 'campaign-creation-success',
31+
};

0 commit comments

Comments
 (0)