diff --git a/src/form/FormikHelpers.tsx b/src/form/FormikHelpers.tsx
index d44f7cf4a..71df7bce8 100644
--- a/src/form/FormikHelpers.tsx
+++ b/src/form/FormikHelpers.tsx
@@ -126,7 +126,7 @@ export const FormikRadioGroup = (
interface FormikRadioControlProps {
name: string;
- options: Array<{ label: string; value: string | number }>;
+ options: Array<{ label: string; value: string | number; disabled?: boolean }>;
label?: string;
helperText?: ReactNode;
disabled?: boolean;
@@ -149,6 +149,7 @@ export const FormikRadioControl = (props: FormikRadioControlProps) => {
key={opt.value}
value={opt.value}
label={opt.label}
+ disabled={opt.disabled}
control={}
/>
))}
diff --git a/src/graphql/advertiser.generated.tsx b/src/graphql/advertiser.generated.tsx
index 47198806c..09b5c837d 100644
--- a/src/graphql/advertiser.generated.tsx
+++ b/src/graphql/advertiser.generated.tsx
@@ -133,7 +133,6 @@ export type AdvertiserPriceFragment = {
billingModelPrice: string;
billingType: Types.BillingType;
format: Types.CampaignFormat;
- isPrimaryFormat: boolean;
};
export type AdvertiserImagesQueryVariables = Types.Exact<{
@@ -162,7 +161,6 @@ export type AdvertiserPricesQuery = {
billingModelPrice: string;
billingType: Types.BillingType;
format: Types.CampaignFormat;
- isPrimaryFormat: boolean;
}>;
} | null;
};
@@ -231,7 +229,6 @@ export const AdvertiserPriceFragmentDoc = gql`
billingModelPrice
billingType
format
- isPrimaryFormat
}
`;
export const AdvertiserDocument = gql`
diff --git a/src/graphql/advertiser.graphql b/src/graphql/advertiser.graphql
index 9aade45b1..d455607bf 100644
--- a/src/graphql/advertiser.graphql
+++ b/src/graphql/advertiser.graphql
@@ -67,7 +67,6 @@ fragment AdvertiserPrice on AdvertiserPrice {
billingModelPrice
billingType
format
- isPrimaryFormat
}
query advertiserImages($id: String!) {
diff --git a/src/graphql/types.ts b/src/graphql/types.ts
index c51133bb8..ae387a3c1 100644
--- a/src/graphql/types.ts
+++ b/src/graphql/types.ts
@@ -44,7 +44,6 @@ export type AdvertiserPriceInput = {
billingModelPrice: Scalars["Numeric"]["input"];
billingType: BillingType;
format: CampaignFormat;
- isPrimaryFormat?: Scalars["Boolean"]["input"];
};
export enum AdvertiserSource {
diff --git a/src/user/views/adsManager/types/index.ts b/src/user/views/adsManager/types/index.ts
index 2878deceb..2e8437ca0 100644
--- a/src/user/views/adsManager/types/index.ts
+++ b/src/user/views/adsManager/types/index.ts
@@ -112,8 +112,9 @@ export const initialCampaign = (
advertiser: AdvertiserWithPrices,
): CampaignForm => {
const format = CampaignFormat.PushNotification;
+ const billingType = "cpm";
const price = advertiser.prices.find(
- (p) => p.format === format && p.isPrimaryFormat,
+ (p) => p.format === format && p.billingType === billingType.toUpperCase(),
);
return {
isCreating: false,
@@ -125,7 +126,7 @@ export const initialCampaign = (
dailyBudget: MIN_PER_CAMPAIGN,
geoTargets: [],
newCreative: initialCreative,
- billingType: (price?.billingType.toLowerCase() ?? "cpm") as Billing,
+ billingType: billingType,
currency: "USD",
price: price?.billingModelPrice ?? "6",
adSets: [
diff --git a/src/user/views/adsManager/views/advanced/components/campaign/BudgetSettings.tsx b/src/user/views/adsManager/views/advanced/components/campaign/BudgetSettings.tsx
index b4175ea00..f804f01e0 100644
--- a/src/user/views/adsManager/views/advanced/components/campaign/BudgetSettings.tsx
+++ b/src/user/views/adsManager/views/advanced/components/campaign/BudgetSettings.tsx
@@ -1,10 +1,11 @@
import { BudgetField } from "user/views/adsManager/views/advanced/components/campaign/fields/BudgetField";
import { PaymentMethodField } from "user/views/adsManager/views/advanced/components/campaign/fields/PaymentMethodField";
+import { AdvertiserPriceFragment } from "graphql/advertiser.generated";
-export function BudgetSettings() {
+export function BudgetSettings(props: { prices: AdvertiserPriceFragment[] }) {
return (
<>
-
+
>
diff --git a/src/user/views/adsManager/views/advanced/components/campaign/fields/BudgetField.tsx b/src/user/views/adsManager/views/advanced/components/campaign/fields/BudgetField.tsx
index df9101587..55c85482b 100644
--- a/src/user/views/adsManager/views/advanced/components/campaign/fields/BudgetField.tsx
+++ b/src/user/views/adsManager/views/advanced/components/campaign/fields/BudgetField.tsx
@@ -4,9 +4,9 @@ import {
FormikTextField,
useIsEdit,
} from "form/FormikHelpers";
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { useField, useFormikContext } from "formik";
-import { CampaignForm } from "../../../../../types";
+import { Billing, CampaignForm } from "../../../../../types";
import { differenceInHours } from "date-fns";
import { MIN_PER_CAMPAIGN, MIN_PER_DAY } from "validation/CampaignSchema";
import { useAdvertiser } from "auth/hooks/queries/useAdvertiser";
@@ -14,10 +14,11 @@ import _ from "lodash";
import { CardContainer } from "components/Card/CardContainer";
import { uiLabelsForBillingType } from "util/billingType";
import { uiTextForCampaignFormat } from "user/library";
-import { CampaignFormat } from "graphql/types";
+import { AdvertiserPriceFragment } from "graphql/advertiser.generated";
-export function BudgetField() {
+export function BudgetField(props: { prices: AdvertiserPriceFragment[] }) {
const [, , dailyBudget] = useField("dailyBudget");
+ const [, , campaignPrice] = useField("price");
const { isDraft } = useIsEdit();
const { advertiser } = useAdvertiser();
const { values, errors } = useFormikContext();
@@ -26,6 +27,18 @@ export function BudgetField() {
differenceInHours(new Date(values.endAt), new Date(values.startAt)) / 24,
);
+ const isBillingTypeDisabled = useCallback(
+ (billing: Billing) => {
+ const billingType = billing.toUpperCase();
+ return (
+ props.prices.find(
+ (p) => p.format === values.format && p.billingType === billingType,
+ ) === undefined
+ );
+ },
+ [values.format, props.prices],
+ );
+
useEffect(() => {
const calculatedBudget =
campaignRuntime > 0
@@ -69,17 +82,38 @@ export function BudgetField() {
disabled={!isDraft && !advertiser.selfServiceSetPrice}
/>
- {!advertiser.selfServiceSetPrice ? (
-
- {uiTextForCampaignFormat(values.format)} campaigns are priced at a
- flat rate of{" "}
-
- ${values.price} {_.upperCase(values.billingType)}
-
- .
-
- ) : (
-
+
+ {
+ const billingType = e.target.value.toUpperCase();
+ const price = props.prices.find(
+ (p) =>
+ p.format === values.format && p.billingType === billingType,
+ );
+ if (price) campaignPrice.setValue(price?.billingModelPrice);
+ }}
+ options={[
+ {
+ value: "cpm",
+ label: uiLabelsForBillingType("cpm").longLabel,
+ disabled: isBillingTypeDisabled("cpm") && !isDraft,
+ },
+ {
+ value: "cpc",
+ label: uiLabelsForBillingType("cpc").longLabel,
+ disabled: isBillingTypeDisabled("cpc") && !isDraft,
+ },
+ {
+ value: "cpqv",
+ label: uiLabelsForBillingType("cpqv").longLabel,
+ disabled: isBillingTypeDisabled("cpqv") && !isDraft,
+ },
+ ]}
+ />
+
+ {advertiser.selfServiceSetPrice ? (
-
-
-
- )}
+ ) : (
+
+ {_.upperCase(values.billingType)}{" "}
+ {uiTextForCampaignFormat(values.format)} campaigns are priced at a
+ flat rate of ${values.price}.
+
+ )}
+
);
diff --git a/src/user/views/adsManager/views/advanced/components/campaign/fields/FormatField.tsx b/src/user/views/adsManager/views/advanced/components/campaign/fields/FormatField.tsx
index 92dcde244..8d7b20475 100644
--- a/src/user/views/adsManager/views/advanced/components/campaign/fields/FormatField.tsx
+++ b/src/user/views/adsManager/views/advanced/components/campaign/fields/FormatField.tsx
@@ -6,6 +6,7 @@ import _ from "lodash";
import { useIsEdit } from "form/FormikHelpers";
import { AdvertiserPriceFragment } from "graphql/advertiser.generated";
import { FormatHelp } from "components/Button/FormatHelp";
+import { Billing } from "user/views/adsManager/types";
interface PriceProps {
prices: AdvertiserPriceFragment[];
@@ -38,6 +39,7 @@ const FormatItemButton = (props: { format: CampaignFormat } & PriceProps) => {
const { isEdit } = useIsEdit();
const [, meta, format] = useField("format");
const [, , price] = useField("price");
+ const [, billingType] = useField("billingType");
return (
{
onClick={() => {
format.setValue(props.format);
const found = props.prices.find(
- (p) => p.format === props.format && p.isPrimaryFormat,
+ (p) =>
+ p.format === props.format &&
+ p.billingType === billingType.value.toUpperCase(),
);
if (props.format === CampaignFormat.NewsDisplayAd) {
price.setValue(found?.billingModelPrice ?? "10");
diff --git a/src/user/views/adsManager/views/advanced/components/form/components/BaseForm.tsx b/src/user/views/adsManager/views/advanced/components/form/components/BaseForm.tsx
index 82571bef7..62edf2bf6 100644
--- a/src/user/views/adsManager/views/advanced/components/form/components/BaseForm.tsx
+++ b/src/user/views/adsManager/views/advanced/components/form/components/BaseForm.tsx
@@ -30,7 +30,7 @@ export function BaseForm({ hasPaymentIntent, prices }: Props) {
{
label: "Budget",
path: `${url}/budget`,
- component: ,
+ component: ,
},
{
label: "Ads",
diff --git a/src/validation/CampaignSchema.test.ts b/src/validation/CampaignSchema.test.ts
index 64847832d..bf7a239c5 100644
--- a/src/validation/CampaignSchema.test.ts
+++ b/src/validation/CampaignSchema.test.ts
@@ -14,13 +14,11 @@ const prices: Omit[] = [
format: CampaignFormat.PushNotification,
billingModelPrice: "6",
billingType: BillingType.Cpm,
- isPrimaryFormat: true,
},
{
format: CampaignFormat.PushNotification,
billingModelPrice: ".15",
billingType: BillingType.Cpc,
- isPrimaryFormat: true,
},
];
diff --git a/src/validation/CampaignSchema.tsx b/src/validation/CampaignSchema.tsx
index 0f318b8d0..35e02e4c5 100644
--- a/src/validation/CampaignSchema.tsx
+++ b/src/validation/CampaignSchema.tsx
@@ -80,7 +80,7 @@ export const CampaignSchema = (prices: AdvertiserPriceFragment[]) =>
then: (schema) =>
findPrice(
prices,
- CampaignFormat.PushNotification,
+ [CampaignFormat.PushNotification],
BillingType.Cpc,
"0.1",
schema,
@@ -92,7 +92,7 @@ export const CampaignSchema = (prices: AdvertiserPriceFragment[]) =>
then: (schema) =>
findPrice(
prices,
- CampaignFormat.PushNotification,
+ [CampaignFormat.PushNotification],
BillingType.Cpm,
"6",
schema,
@@ -104,16 +104,30 @@ export const CampaignSchema = (prices: AdvertiserPriceFragment[]) =>
then: (schema) =>
findPrice(
prices,
- CampaignFormat.NewsDisplayAd,
+ [CampaignFormat.NewsDisplayAd],
BillingType.Cpm,
"10",
schema,
),
})
+ .when(["billingType", "format"], {
+ is: (b: string, f: CampaignFormat) =>
+ b === "cpqv" &&
+ (f === CampaignFormat.NewsDisplayAd ||
+ CampaignFormat.PushNotification),
+ then: (schema) =>
+ findPrice(
+ prices,
+ [CampaignFormat.NewsDisplayAd, CampaignFormat.PushNotification],
+ BillingType.Cpqv,
+ "1.5",
+ schema,
+ ),
+ })
.required("Price is a required field"),
billingType: string()
.label("Pricing Type")
- .oneOf(["cpm", "cpc"])
+ .oneOf(["cpm", "cpc", "cpqv"])
.required("Pricing type is a required field"),
adSets: array()
.min(1)
@@ -177,14 +191,13 @@ export const CampaignSchema = (prices: AdvertiserPriceFragment[]) =>
export function findPrice(
prices: AdvertiserPriceFragment[],
- format: CampaignFormat,
+ formats: CampaignFormat[],
billingType: BillingType,
defaultPrice: string,
schema: StringSchema,
) {
const found = prices.find(
- (p) =>
- p.format === format && p.billingType === billingType && p.isPrimaryFormat,
+ (p) => formats.includes(p.format) && p.billingType === billingType,
);
const price = BigNumber(found?.billingModelPrice ?? defaultPrice);
return schema.test(