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(