From ceab9c6f5b02dcaf380b81788ed856ad09b81409 Mon Sep 17 00:00:00 2001 From: Ian Krieger Date: Fri, 18 Aug 2023 17:20:39 -0400 Subject: [PATCH] chore: move away from notification specific code --- src/components/Box/BoxContainer.tsx | 2 +- .../Creatives/CreativeAutocomplete.tsx | 96 -------------- src/components/Creatives/CreativeFields.tsx | 73 ----------- src/components/Creatives/CreativeList.tsx | 89 ------------- .../Creatives/CreativeSpecificFields.tsx | 17 +++ .../Creatives/CreativeTypePreview.tsx | 18 --- src/components/Creatives/NewCreative.tsx | 119 ------------------ .../Creatives/NotificationFields.tsx | 30 ----- .../Creatives/NotificationPreview.tsx | 14 ++- .../Creatives/NotificationSelect.tsx | 60 +++++++++ .../Creatives/SelectCreativeHeader.tsx | 81 ++++++++++++ src/components/Drawer/MiniSideBar.tsx | 2 +- src/form/PersistFormValues.tsx | 11 +- src/graphql/ad-set.generated.tsx | 6 +- src/graphql/campaign.generated.tsx | 8 +- src/graphql/types.ts | 16 +-- src/user/User.tsx | 13 -- src/user/ads/AdsNewAd.tsx | 114 ++++++++++++----- src/user/ads/NewAd.tsx | 104 +++++++-------- src/user/ads/NotificationAd.tsx | 70 ++++------- src/user/hooks/useAdvertiserCreatives.ts | 51 ++++++-- src/user/library/index.test.ts | 11 +- src/user/library/index.ts | 75 ++++------- src/user/views/adsManager/types/index.ts | 41 ++---- .../advanced/components/adSet/AdSetFields.tsx | 1 + .../components/adSet/fields/AdSetAds.tsx | 49 ++------ .../campaign/fields/BudgetField.tsx | 4 +- .../advanced/components/form/EditCampaign.tsx | 11 +- .../components/form/components/BaseForm.tsx | 10 +- .../form/components/PaymentButton.tsx | 12 +- .../advanced/components/review/Review.tsx | 3 - .../components/review/components/AdReview.tsx | 29 ----- .../review/components/CampaignReview.tsx | 2 +- src/validation/CampaignSchema.tsx | 39 +----- 34 files changed, 471 insertions(+), 810 deletions(-) delete mode 100644 src/components/Creatives/CreativeAutocomplete.tsx delete mode 100644 src/components/Creatives/CreativeFields.tsx delete mode 100644 src/components/Creatives/CreativeList.tsx create mode 100644 src/components/Creatives/CreativeSpecificFields.tsx delete mode 100644 src/components/Creatives/CreativeTypePreview.tsx delete mode 100644 src/components/Creatives/NewCreative.tsx delete mode 100644 src/components/Creatives/NotificationFields.tsx create mode 100644 src/components/Creatives/NotificationSelect.tsx create mode 100644 src/components/Creatives/SelectCreativeHeader.tsx delete mode 100644 src/user/views/adsManager/views/advanced/components/review/components/AdReview.tsx diff --git a/src/components/Box/BoxContainer.tsx b/src/components/Box/BoxContainer.tsx index 4b152a489..d3987a307 100644 --- a/src/components/Box/BoxContainer.tsx +++ b/src/components/Box/BoxContainer.tsx @@ -11,7 +11,7 @@ export function BoxContainer( {props.header} )} - + {props.children} diff --git a/src/components/Creatives/CreativeAutocomplete.tsx b/src/components/Creatives/CreativeAutocomplete.tsx deleted file mode 100644 index 0eed9a741..000000000 --- a/src/components/Creatives/CreativeAutocomplete.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { - Autocomplete, - Box, - Button, - Checkbox, - TextField, - Typography, -} from "@mui/material"; -import { CreativeFragment } from "graphql/creative.generated"; -import { uiTextForCreativeTypeCode } from "user/library"; -import CheckBoxIcon from "@mui/icons-material/CheckBox"; -import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"; -import moment from "moment"; -import { useFormikContext } from "formik"; -import { CampaignForm } from "user/views/adsManager/types"; -import _ from "lodash"; -import { useEffect, useState } from "react"; - -interface CreativeAutocompleteProps { - label: string; - options: readonly CreativeFragment[]; - alreadyAssociatedCreativeIds: string[]; - onSetValue: () => void; -} - -export function CreativeAutocomplete(params: CreativeAutocompleteProps) { - const { setFieldValue } = useFormikContext(); - const label = params.label; - const [alreadyAdded, setAlreadyAdded] = useState([]); - - useEffect(() => { - setAlreadyAdded(params.alreadyAssociatedCreativeIds); - }, [params.alreadyAssociatedCreativeIds]); - - return ( - - { - const mapped = value.map((c) => c.id); - setAlreadyAdded(mapped); - await setFieldValue("creatives", _.uniq(mapped)); - }} - value={params.options.filter((o) => alreadyAdded.includes(o.id))} - renderInput={(params) => ( - - )} - renderOption={(props, option, { selected }) => { - return ( -
  • - } - checkedIcon={} - style={{ marginRight: 8 }} - checked={ - selected || - params.alreadyAssociatedCreativeIds.includes(option?.id) - } - /> - {option.name} - - created {moment(option.createdAt).fromNow()} - -
  • - ); - }} - getOptionLabel={(opt) => opt?.name ?? ""} - getOptionDisabled={(opt) => - params.alreadyAssociatedCreativeIds.includes(opt?.id) - } - groupBy={(opt) => uiTextForCreativeTypeCode(opt.type)} - /> - -
    - ); -} diff --git a/src/components/Creatives/CreativeFields.tsx b/src/components/Creatives/CreativeFields.tsx deleted file mode 100644 index b38c6683a..000000000 --- a/src/components/Creatives/CreativeFields.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { - FormControl, - Box, - FormLabel, - ListItemButton, - List, -} from "@mui/material"; -import { useFormikContext } from "formik"; -import { CreativeInput } from "graphql/types"; -import { NotificationFields } from "./NotificationFields"; -import { FormikTextField } from "form/FormikHelpers"; - -interface Props { - allowTypeChange: boolean; -} - -export function CreativeFields({ allowTypeChange }: Props) { - const formik = useFormikContext(); - - const supportedTypes = [ - { - value: "notification_all_v1", - label: "Push Notification", - }, - ]; - - return ( - <> - - - - - - Creative Type - - - {supportedTypes.map((s) => ( - formik.setFieldValue("type.code", s.value)} - > - {s.label} - - ))} - - - - - - - ); -} - -const CreativeTypeSpecificFields = ({ - creativeType, -}: { - creativeType?: string; -}) => { - if (creativeType === "notification_all_v1") return ; - - return null; -}; diff --git a/src/components/Creatives/CreativeList.tsx b/src/components/Creatives/CreativeList.tsx deleted file mode 100644 index 9f05b1196..000000000 --- a/src/components/Creatives/CreativeList.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { EnhancedTable, StandardRenderers } from "components/EnhancedTable"; -import { useAdvertiserCreativesQuery } from "graphql/creative.generated"; -import { uiTextForCreativeTypeCode } from "user/library"; -import { CardContainer } from "components/Card/CardContainer"; -import { useAdvertiser } from "auth/hooks/queries/useAdvertiser"; -import { ErrorDetail } from "components/Error/ErrorDetail"; -import MiniSideBar from "components/Drawer/MiniSideBar"; -import { Skeleton } from "@mui/material"; - -export function CreativeList() { - const { advertiser } = useAdvertiser(); - const { data, error, loading } = useAdvertiserCreativesQuery({ - variables: { - advertiserId: advertiser.id, - }, - pollInterval: 60_000, - }); - - if (error) - return ( - - ); - - if (loading) { - return ( - - - - - - ); - } - - return ( - - - c.modifiedAt, - renderer: StandardRenderers.date, - }, - { - title: "Name", - value: (c) => c.name, - }, - { - title: "Type", - value: (c) => uiTextForCreativeTypeCode(c.type), - }, - { - title: "Title", - value: (c) => - c.payloadInlineContent?.title ?? - c.payloadNotification?.title ?? - c.payloadSearch?.title ?? - c.payloadSearchHomepage?.title, - }, - { - title: "Body", - value: (c) => - c.payloadInlineContent?.ctaText ?? - c.payloadNotification?.body ?? - c.payloadSearch?.body ?? - c.payloadSearchHomepage?.body, - }, - ]} - /> - - - ); -} diff --git a/src/components/Creatives/CreativeSpecificFields.tsx b/src/components/Creatives/CreativeSpecificFields.tsx new file mode 100644 index 000000000..b32b32569 --- /dev/null +++ b/src/components/Creatives/CreativeSpecificFields.tsx @@ -0,0 +1,17 @@ +import { useFormikContext } from "formik"; +import { CampaignForm } from "user/views/adsManager/types"; +import { CampaignFormat } from "graphql/types"; +import { NotificationAd } from "user/ads/NotificationAd"; + +interface Props { + onCreate: () => void; +} + +export const CreativeSpecificFields = ({ onCreate }: Props) => { + const { values } = useFormikContext(); + + if (values.format === CampaignFormat.PushNotification) + return ; + + return null; +}; diff --git a/src/components/Creatives/CreativeTypePreview.tsx b/src/components/Creatives/CreativeTypePreview.tsx deleted file mode 100644 index c609b785b..000000000 --- a/src/components/Creatives/CreativeTypePreview.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useFormikContext } from "formik"; -import { CreativeInput } from "graphql/types"; -import { NotificationPreview } from "components/Creatives/NotificationPreview"; - -export const CreativeTypePreview = () => { - const { values } = useFormikContext(); - - if (values.type.code === "notification_all_v1") { - return ( - - ); - } - - return null; -}; diff --git a/src/components/Creatives/NewCreative.tsx b/src/components/Creatives/NewCreative.tsx deleted file mode 100644 index 21471a5e2..000000000 --- a/src/components/Creatives/NewCreative.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { Box, Container, Snackbar } from "@mui/material"; -import { Form, Formik } from "formik"; -import { useHistory, useLocation } from "react-router-dom"; -import { CreativeInput } from "graphql/types"; -import { CreativeFields } from "./CreativeFields"; -import { - AdvertiserCreativesDocument, - useCreateCreativeMutation, -} from "graphql/creative.generated"; -import { CardContainer } from "components/Card/CardContainer"; -import { ErrorDetail } from "components/Error/ErrorDetail"; -import { FormikSubmitButton } from "form/FormikHelpers"; -import { CreativeSchema } from "validation/CreativeSchema"; -import MiniSideBar from "components/Drawer/MiniSideBar"; -import { - clearCreativeValues, - PersistCreativeValues, -} from "form/PersistCreativeValues"; -import { CreativeTypePreview } from "components/Creatives/CreativeTypePreview"; -import { useAdvertiser } from "auth/hooks/queries/useAdvertiser"; -import { useState } from "react"; - -export function NewCreative() { - const [id, setId] = useState(); - const { advertiser } = useAdvertiser(); - const history = useHistory(); - const location = useLocation(); - - const defaultValue: CreativeInput & { targetUrlValid: boolean } = { - advertiserId: "", - state: "under_review", - name: "", - type: { - code: "notification_all_v1", - }, - targetUrlValid: false, - payloadNotification: { - body: "", - targetUrl: "", - title: "", - }, - }; - - const initialValue = location.state ?? defaultValue; - - const [createCreativeMutation, { error }] = useCreateCreativeMutation({ - refetchQueries: [ - { - query: AdvertiserCreativesDocument, - variables: { advertiserId: advertiser.id }, - }, - ], - onCompleted(data) { - setId(data.createCreative.id); - history.replace("/user/main/creatives"); - clearCreativeValues(); - }, - }); - - const doSubmit = async (values: CreativeInput) => { - const input: CreativeInput = { - advertiserId: advertiser.id, - name: values.name, - payloadNotification: values.payloadNotification, - state: values.state, - type: values.type, - }; - - void createCreativeMutation({ - variables: { input }, - }); - }; - - return ( - - - - - -
    - - - - - - - - - - -
    - - - - -
    -
    - - -
    -
    - ); -} diff --git a/src/components/Creatives/NotificationFields.tsx b/src/components/Creatives/NotificationFields.tsx deleted file mode 100644 index 854ff55d7..000000000 --- a/src/components/Creatives/NotificationFields.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { FormikTextField } from "form/FormikHelpers"; -import { UrlResolver } from "components/Url/UrlResolver"; -import { Stack } from "@mui/material"; - -export function NotificationFields() { - return ( - <> - - - - - - - - - ); -} diff --git a/src/components/Creatives/NotificationPreview.tsx b/src/components/Creatives/NotificationPreview.tsx index be2af788d..b4b79ba60 100644 --- a/src/components/Creatives/NotificationPreview.tsx +++ b/src/components/Creatives/NotificationPreview.tsx @@ -1,10 +1,10 @@ -import { useField } from "formik"; import { Box, Paper, Stack, Typography } from "@mui/material"; import logo from "../../../brave_logo_icon.png"; -import { Creative } from "user/views/adsManager/types"; +import { useField } from "formik"; +import { CreativeInput } from "graphql/types"; export function NotificationPreview(props: { title?: string; body?: string }) { - const [, meta] = useField("newCreative"); + const [, meta, ,] = useField("newCreative"); return ( @@ -27,10 +27,14 @@ export function NotificationPreview(props: { title?: string; body?: string }) { /> - {props.title || meta.value?.title || "Title Preview"} + {props.title || + meta.value?.payloadNotification?.title || + "Title Preview"} - {props.body || meta.value?.body || "Body Preview"} + {props.body || + meta.value?.payloadNotification?.body || + "Body Preview"} diff --git a/src/components/Creatives/NotificationSelect.tsx b/src/components/Creatives/NotificationSelect.tsx new file mode 100644 index 000000000..9ee89d08b --- /dev/null +++ b/src/components/Creatives/NotificationSelect.tsx @@ -0,0 +1,60 @@ +import { LinearProgress, Stack, Typography } from "@mui/material"; +import { BoxContainer } from "components/Box/BoxContainer"; +import { NotificationPreview } from "components/Creatives/NotificationPreview"; +import moment from "moment"; +import { useField } from "formik"; +import { Creative } from "user/views/adsManager/types"; +import { CreativeFragment } from "graphql/creative.generated"; +import { SelectCreativeHeader } from "components/Creatives/SelectCreativeHeader"; + +export function NotificationSelect(props: { + options: CreativeFragment[]; + fieldName: string; + loading?: boolean; +}) { + const [, meta] = useField(props.fieldName); + const mapped = meta.value.map((m) => m.id); + const sorted = props.options.sort((a, b) => { + if (mapped.includes(a.id) && !mapped.includes(b.id)) return -1; + if (!mapped.includes(a.id) && mapped.includes(b.id)) return 1; + + return a.createdAt - b.createdAt; + }); + + if (props.loading) { + return ; + } + + return ( + 3 ? "scroll" : "hidden" }} + > + {sorted.map((co, idx) => ( + + } + key={idx} + > + + + created {moment(co.createdAt).fromNow()} + + + ))} + + ); +} diff --git a/src/components/Creatives/SelectCreativeHeader.tsx b/src/components/Creatives/SelectCreativeHeader.tsx new file mode 100644 index 000000000..97166d527 --- /dev/null +++ b/src/components/Creatives/SelectCreativeHeader.tsx @@ -0,0 +1,81 @@ +import { CreativeFragment } from "graphql/creative.generated"; +import { useField, useFormikContext } from "formik"; +import { CampaignForm, Creative } from "user/views/adsManager/types"; +import { useAdvertiser } from "auth/hooks/queries/useAdvertiser"; +import { useEffect, useState } from "react"; +import _ from "lodash"; +import { Box, IconButton, Tooltip } from "@mui/material"; +import CheckBoxIcon from "@mui/icons-material/CheckBox"; +import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"; + +export const SelectCreativeHeader = (props: { + creative: CreativeFragment; + fieldName: string; +}) => { + const { values } = useFormikContext(); + const { advertiser } = useAdvertiser(); + const [, meta, helpers] = useField(props.fieldName); + const [selected, setSelected] = useState(false); + const [disabled, setDisabled] = useState(false); + + const onSelectCreative = ( + c: CreativeFragment, + v: Creative[] | undefined, + selected: boolean, + ) => { + let value; + if (selected) { + value = [...(v ?? []), c]; + } else { + value = _.filter(v ?? [], (n) => n.id !== c.id); + } + const mapped = value.map((d) => ({ advertiserId: advertiser.id, ...d })); + helpers.setValue(mapped); + }; + + useEffect(() => { + setSelected( + (meta.value ?? []).find((c) => c.id === props.creative.id) !== undefined, + ); + setDisabled( + props.fieldName === "creatives" && + _.flatMap(values.adSets, "creatives").some( + (v) => v.id === props.creative.id, + ), + ); + }, [meta.value]); + + return ( + + {props.creative.name} + + + { + const s = !selected; + setSelected(s); + onSelectCreative(props.creative, meta.value, s); + }} + sx={{ p: 0 }} + disabled={disabled} + > + {selected ? ( + + ) : ( + + )} + + + + + ); +}; diff --git a/src/components/Drawer/MiniSideBar.tsx b/src/components/Drawer/MiniSideBar.tsx index 53e84d29e..773c4815f 100644 --- a/src/components/Drawer/MiniSideBar.tsx +++ b/src/components/Drawer/MiniSideBar.tsx @@ -52,8 +52,8 @@ export default function MiniSideBar({ children }: PropsWithChildren) { sx={{ color: "text.secondary" }} /> ), + disabled: true, }, - // Possible future enhancements, not visible to user but help keep spacing { label: "Assets", href: "/user/main/assets", diff --git a/src/form/PersistFormValues.tsx b/src/form/PersistFormValues.tsx index e21ad13e6..cbf2d268c 100644 --- a/src/form/PersistFormValues.tsx +++ b/src/form/PersistFormValues.tsx @@ -18,7 +18,7 @@ export const PersistFormValues = () => { // read the values from localStorage on load useEffect(() => { - setForm(values.draftId); + setForm(values.draftId ?? values.id); }, []); // save the values to localStorage on update @@ -26,7 +26,14 @@ export const PersistFormValues = () => { if (values.draftId && dirty) { localStorage.setItem(values.draftId, JSON.stringify(values)); } - setDrafts(); + + if (values.id && dirty) { + localStorage.setItem(values.id, JSON.stringify(values)); + } + + if (!values.id) { + setDrafts(); + } }, [values, dirty]); return null; diff --git a/src/graphql/ad-set.generated.tsx b/src/graphql/ad-set.generated.tsx index 6a95a7d0c..73720cab0 100644 --- a/src/graphql/ad-set.generated.tsx +++ b/src/graphql/ad-set.generated.tsx @@ -13,7 +13,7 @@ export type AdSetFragment = { totalMax: number; perDay: number; state: string; - execution: string; + execution?: string | null; keywords?: Array | null; keywordSimilarity?: number | null; negativeKeywords?: Array | null; @@ -171,7 +171,7 @@ export type CreateAdSetMutation = { totalMax: number; perDay: number; state: string; - execution: string; + execution?: string | null; keywords?: Array | null; keywordSimilarity?: number | null; negativeKeywords?: Array | null; @@ -268,7 +268,7 @@ export type UpdateAdSetMutation = { totalMax: number; perDay: number; state: string; - execution: string; + execution?: string | null; keywords?: Array | null; keywordSimilarity?: number | null; negativeKeywords?: Array | null; diff --git a/src/graphql/campaign.generated.tsx b/src/graphql/campaign.generated.tsx index d63848dcf..24b8d9776 100644 --- a/src/graphql/campaign.generated.tsx +++ b/src/graphql/campaign.generated.tsx @@ -49,7 +49,7 @@ export type CampaignFragment = { totalMax: number; perDay: number; state: string; - execution: string; + execution?: string | null; keywords?: Array | null; keywordSimilarity?: number | null; negativeKeywords?: Array | null; @@ -177,7 +177,7 @@ export type CampaignAdsFragment = { totalMax: number; perDay: number; state: string; - execution: string; + execution?: string | null; keywords?: Array | null; keywordSimilarity?: number | null; negativeKeywords?: Array | null; @@ -310,7 +310,7 @@ export type LoadCampaignQuery = { totalMax: number; perDay: number; state: string; - execution: string; + execution?: string | null; keywords?: Array | null; keywordSimilarity?: number | null; negativeKeywords?: Array | null; @@ -420,7 +420,7 @@ export type LoadCampaignAdsQuery = { totalMax: number; perDay: number; state: string; - execution: string; + execution?: string | null; keywords?: Array | null; keywordSimilarity?: number | null; negativeKeywords?: Array | null; diff --git a/src/graphql/types.ts b/src/graphql/types.ts index 810f71d69..1c2e0c058 100644 --- a/src/graphql/types.ts +++ b/src/graphql/types.ts @@ -126,7 +126,7 @@ export type CreateAdSetInput = { campaignId?: InputMaybe; channels?: InputMaybe>; conversions?: InputMaybe>; - execution: Scalars["String"]; + execution?: InputMaybe; keywordSimilarity?: InputMaybe; keywords?: InputMaybe>; name?: InputMaybe; @@ -401,7 +401,7 @@ export type UpdateAdSetInput = { bannedKeywords?: InputMaybe>; billingType?: InputMaybe; campaignId?: InputMaybe; - channels?: InputMaybe>; + channels?: InputMaybe>; conversions?: InputMaybe>; execution?: InputMaybe; id?: InputMaybe; @@ -409,6 +409,7 @@ export type UpdateAdSetInput = { keywords?: InputMaybe>; name?: InputMaybe; negativeKeywords?: InputMaybe>; + optimized?: InputMaybe; oses?: InputMaybe>; perDay?: InputMaybe; segments?: InputMaybe>; @@ -472,17 +473,13 @@ export type UpdateCampaignInput = { type?: InputMaybe; }; -export type UpdateChannelsInput = { - channelId: Scalars["String"]; -}; - export type UpdateConversionsInput = { extractExternalId?: InputMaybe; id?: InputMaybe; - observationWindow: Scalars["Float"]; + observationWindow?: InputMaybe; trailingAsteriskNotRequired?: InputMaybe; - type: Scalars["String"]; - urlPattern: Scalars["String"]; + type?: InputMaybe; + urlPattern?: InputMaybe; }; export type UpdateInPageCreativeInput = { @@ -529,7 +526,6 @@ export type UpdateOSesInput = { export type UpdateSegmentInput = { code?: InputMaybe; name?: InputMaybe; - state?: InputMaybe; }; export type UpdateUserInput = { diff --git a/src/user/User.tsx b/src/user/User.tsx index b54f799ae..41ff4d2e5 100644 --- a/src/user/User.tsx +++ b/src/user/User.tsx @@ -19,8 +19,6 @@ import { CampaignView } from "user/views/user/CampaignView"; import { CampaignReportView } from "user/views/user/CampaignReportView"; import { Profile } from "user/views/user/Profile"; import { IAdvertiser } from "auth/context/auth.interface"; -import { CreativeList } from "components/Creatives/CreativeList"; -import { NewCreative } from "components/Creatives/NewCreative"; const buildApolloClient = () => { const httpLink = createHttpLink({ @@ -62,12 +60,6 @@ export function User() { validateAdvertiserProperty={(a) => a.selfServiceEdit} /> - a.selfServiceCreate} - /> - - - void }) { +export function AdsExistingAd() { const { values } = useFormikContext(); const { advertiser } = useAdvertiser(); - const { data } = useAdvertiserCreativesQuery({ + const original = useRef([]); + const [options, setOptions] = useState([]); + const { loading } = useAdvertiserCreativesQuery({ variables: { advertiserId: advertiser.id }, - }); + onCompleted(data) { + const creativeOptionList = _.orderBy( + filterCreativesBasedOnCampaignFormat( + data.advertiser?.creatives ?? [], + values.format, + ), + ["type.code", "createdAt"], + ["asc", "desc"], + ) as CreativeFragment[]; - const allCreativesForAdvertiser = data?.advertiser?.creatives ?? []; - const associatedCreatives = values.creatives ?? []; - const creativeOptionList = _.orderBy( - filterCreativesBasedOnCampaignFormat( - allCreativesForAdvertiser, - values.format, - ), - ["type.code", "createdAt"], - ["asc", "desc"], - ) as CreativeFragment[]; + original.current = creativeOptionList; + setOptions(creativeOptionList); + }, + }); return ( - - Add an existing creative + + + Add an existing Ad + - + + + Ads are modular building blocks that can be paired with ad sets to + build unique combinations.
    Select by using the box next to the + name. +
    - - Creatives are modular building blocks that can be paired with ad sets to - build ads. - + + + + ), + }} + onChange={(e) => { + const value = e.target.value.toLowerCase(); + if (!value || value.trim() !== "") { + setOptions( + original.current.filter((co) => + co.name.toLowerCase().includes(value), + ), + ); + } else { + setOptions(original.current); + } + }} + /> +
    + - { - props.onAddCreative(); - }} +
    ); } + +const CreativeSpecificSelect = (props: { + format: CampaignFormat; + options: CreativeFragment[]; + loading: boolean; +}) => { + if (props.format === CampaignFormat.PushNotification) + return ( + + ); + + return null; +}; diff --git a/src/user/ads/NewAd.tsx b/src/user/ads/NewAd.tsx index 8ea525fbc..70eabc304 100644 --- a/src/user/ads/NewAd.tsx +++ b/src/user/ads/NewAd.tsx @@ -1,37 +1,30 @@ -import { useRecentlyCreatedAdvertiserCreatives } from "user/hooks/useAdvertiserCreatives"; import { CardContainer } from "components/Card/CardContainer"; -import { Box, Button, IconButton, Link, Stack } from "@mui/material"; +import { Box, Button, Stack } from "@mui/material"; import { useState } from "react"; import { BoxContainer } from "components/Box/BoxContainer"; import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline"; -import { NotificationAd } from "user/ads/NotificationAd"; -import { useField, useFormikContext } from "formik"; +import { AdsExistingAd } from "user/ads/AdsNewAd"; +import { CreativeSpecificFields } from "components/Creatives/CreativeSpecificFields"; +import { useFreshCreatives } from "user/hooks/useAdvertiserCreatives"; +import { useFormikContext } from "formik"; +import { CampaignForm } from "user/views/adsManager/types"; +import { CampaignFormat } from "graphql/types"; import { NotificationPreview } from "components/Creatives/NotificationPreview"; -import { AdsNewAd } from "user/ads/AdsNewAd"; -import { CampaignForm, Creative } from "user/views/adsManager/types"; -import _ from "lodash"; export function NewAd() { - const [, meta, helper] = useField("isCreating"); const [showForm, setShowForm] = useState(false); - const [useExisting, setUseExisting] = useState(false); - const creatives = useRecentlyCreatedAdvertiserCreatives(); return ( <> - + - {(creatives ?? []).map((c, idx) => ( - } key={idx}> - - - ))} + { - helper.setValue(!meta.value); setShowForm(!showForm); - setUseExisting(false); }} > {showForm ? ( @@ -53,51 +44,52 @@ export function NewAd() { - {!useExisting && ( - { - setUseExisting(true); - setShowForm(false); - }} - underline="none" - sx={{ cursor: "pointer" }} - > - Choose a previously made Creative - - )} - {useExisting && ( - setUseExisting(!useExisting)} /> - )} + {!showForm && } {showForm && ( - { - helper.setValue(false); - setShowForm(false); - }} - /> + setShowForm(false)} /> )} ); } -const RemoveHeader = (props: { creative: Creative }) => { - const { values, setFieldValue } = useFormikContext(); +const SpecificNewCreative = () => { + const { values } = useFormikContext(); + const creatives = useFreshCreatives(); - const onRemoveCreative = async (c: Creative, v: string[] | undefined) => { - const removed = _.filter(v ?? [], (n) => n !== c.id); - void setFieldValue("creatives", removed); - }; + if (creatives.length === 0) { + return null; + } - return ( - - {props.creative.name} - onRemoveCreative(props.creative, values.creatives)} - sx={{ p: 0 }} - > - - - - ); + if (values.format === CampaignFormat.PushNotification) { + return creatives.map((c, idx) => ( + + + + )); + } }; + +// const RemoveHeader = (props: { creative: Creative }) => { +// const { values, setFieldValue } = useFormikContext(); +// +// const onRemoveCreative = async (c: Creative, v: string[] | undefined) => { +// const removed = _.filter(v ?? [], (n) => n !== c.id); +// void setFieldValue("creatives", removed); +// }; +// +// return ( +// +// {props.creative.name} +// onRemoveCreative(props.creative, values.creatives)} +// sx={{ p: 0 }} +// > +// +// +// +// ); +// }; diff --git a/src/user/ads/NotificationAd.tsx b/src/user/ads/NotificationAd.tsx index 9b9e58de4..4b761bbcb 100644 --- a/src/user/ads/NotificationAd.tsx +++ b/src/user/ads/NotificationAd.tsx @@ -1,46 +1,32 @@ import { CardContainer } from "components/Card/CardContainer"; import { FormikTextField } from "form/FormikHelpers"; -import { Stack } from "@mui/material"; +import { Button, Stack } from "@mui/material"; import { UrlResolver } from "components/Url/UrlResolver"; -import { LoadingButton } from "@mui/lab"; import SaveIcon from "@mui/icons-material/Save"; -import { creativeInput } from "user/library"; -import { CreateNotificationCreativeInput } from "graphql/types"; import { useField } from "formik"; import { Creative, initialCreative } from "user/views/adsManager/types"; import { useAdvertiser } from "auth/hooks/queries/useAdvertiser"; -import { useUser } from "auth/hooks/queries/useUser"; -import { - refetchAdvertiserCreativesQuery, - useCreateNotificationCreativeMutation, -} from "graphql/creative.generated"; import { NotificationPreview } from "components/Creatives/NotificationPreview"; +import _ from "lodash"; +import { useEffect } from "react"; interface Props { onCreate: () => void; } export function NotificationAd({ onCreate }: Props) { - const [, meta, newCreativeHelper] = useField("newCreative"); - const [, creativesMeta, creativesHelper] = useField("creatives"); + const [, newMeta, newHelper] = useField("newCreative"); + const [, creativesMeta, creativesHelper] = useField("creatives"); const { advertiser } = useAdvertiser(); - const { userId } = useUser(); - const [create, { loading }] = useCreateNotificationCreativeMutation({ - async onCompleted(data) { - newCreativeHelper.setValue(initialCreative); - newCreativeHelper.setTouched(false); - creativesHelper.setValue([ - ...(creativesMeta.value ?? []), - data.createNotificationCreative.id, - ]); - onCreate(); - }, - refetchQueries: [ - { - ...refetchAdvertiserCreativesQuery({ advertiserId: advertiser.id }), - }, - ], - }); + + useEffect(() => { + newHelper.setValue({ + ...newMeta.value, + advertiserId: advertiser.id, + state: "draft", + type: { code: "notification_all_v1" }, + }); + }, []); return ( @@ -48,14 +34,14 @@ export function NotificationAd({ onCreate }: Props) {
    - } onClick={(e) => { e.preventDefault(); - const input = creativeInput( - advertiser.id, - meta.value, - userId, - ) as CreateNotificationCreativeInput; - create({ variables: { input } }); + creativesHelper.setValue([...creativesMeta.value, newMeta.value]); + newHelper.setValue(initialCreative); + newHelper.setTouched(false, false); + onCreate(); }} disabled={ - !!meta.error || - meta.value?.targetUrlValidationResult !== undefined || - loading + !_.isEmpty(newMeta.error) || + newMeta.value?.targetUrlValid !== undefined } - loading={loading} > Add - + ); diff --git a/src/user/hooks/useAdvertiserCreatives.ts b/src/user/hooks/useAdvertiserCreatives.ts index 5db502a68..3a0cd0f22 100644 --- a/src/user/hooks/useAdvertiserCreatives.ts +++ b/src/user/hooks/useAdvertiserCreatives.ts @@ -1,22 +1,22 @@ -import { useAdvertiserCreativesQuery } from "graphql/creative.generated"; +import { + CreativeFragment, + useAdvertiserCreativesQuery, +} from "graphql/creative.generated"; import { useAdvertiser } from "auth/hooks/queries/useAdvertiser"; import { useFormikContext } from "formik"; -import { CampaignForm, Creative } from "user/views/adsManager/types"; +import { CampaignForm } from "user/views/adsManager/types"; import _ from "lodash"; +import moment from "moment"; -export function useAdvertiserCreatives(): Creative[] { +export function useAdvertiserCreatives(): CreativeFragment[] { const { advertiser } = useAdvertiser(); const { data } = useAdvertiserCreativesQuery({ variables: { advertiserId: advertiser.id }, }); return (data?.advertiser?.creatives ?? []).map((c) => ({ - id: c.id, - name: c.name, - title: c.payloadNotification?.title ?? "New Ad", - body: c.payloadNotification?.body ?? "Body Preview", - targetUrl: c.payloadNotification?.targetUrl ?? "", - state: c.state, + ...c, + advertiserId: advertiser.id, })); } @@ -25,7 +25,11 @@ export function useRecentlyCreatedAdvertiserCreatives() { const creatives = useAdvertiserCreatives(); const inCampaign = creatives.filter((c) => { if (c.id) { - return (values.creatives ?? []).includes(c.id); + return ( + (_.flatMap(values.adSets, "creatives") ?? []).find( + (i) => i.id === c.id, + ) !== undefined + ); } return false; @@ -33,3 +37,30 @@ export function useRecentlyCreatedAdvertiserCreatives() { return _.uniqBy(inCampaign, "id"); } + +export function useFreshCreatives() { + const { values } = useFormikContext(); + return (values.creatives ?? []).filter((c) => !c.id); +} + +export function useRecentlyCreatedAdSetAds() { + const { values } = useFormikContext(); + const creatives = useAdvertiserCreatives(); + const brandNew = (values.creatives ?? []) + .filter((c) => !c.id) + .map((c) => ({ + ...c, + createdAt: moment(), + modifiedAt: moment(), + })) as CreativeFragment[]; + + const inAdSet = creatives.filter((c) => { + if (c.id) { + return (values.creatives ?? []).find((i) => i.id === c.id) !== undefined; + } + + return false; + }); + + return [..._.uniqBy(inAdSet, "id"), ...brandNew]; +} diff --git a/src/user/library/index.test.ts b/src/user/library/index.test.ts index aad02ce5a..b1215e2fc 100644 --- a/src/user/library/index.test.ts +++ b/src/user/library/index.test.ts @@ -160,10 +160,15 @@ describe("pricing logic (read)", () => { describe("pricing logic (write)", () => { const creative: Creative = { + payloadNotification: { + title: "some title", + body: "body", + targetUrl: "some url", + }, + advertiserId: "some id", + state: "draft", + type: { code: "notification_all_v1" }, name: "some name", - title: "some title", - body: "body", - targetUrl: "https://some.example.org", }; it("should convert from CPM to per-impression values when populating a CPM creative", () => { diff --git a/src/user/library/index.ts b/src/user/library/index.ts index 216ca28ed..9b00c9164 100644 --- a/src/user/library/index.ts +++ b/src/user/library/index.ts @@ -1,12 +1,12 @@ import { CampaignFormat, + CampaignPacingStrategies, ConfirmationType, CreateAdInput, CreateCampaignInput, - CreateNotificationCreativeInput, + CreativeInput, GeocodeInput, UpdateCampaignInput, - UpdateNotificationCreativeInput, } from "graphql/types"; import { CampaignFragment } from "graphql/campaign.generated"; import { AdFragment } from "graphql/ad-set.generated"; @@ -35,15 +35,14 @@ export function transformNewForm( userId?: string, ): CreateCampaignInput { return { - currency: form.currency, - dailyCap: form.dailyCap, + currency: "USD", + dailyCap: 1, dailyBudget: form.dailyBudget, endAt: form.endAt, - pacingStrategy: form.pacingStrategy, + pacingStrategy: CampaignPacingStrategies.ModelV1, geoTargets: form.geoTargets.map((g) => ({ code: g.code, name: g.name })), name: form.name, advertiserId: form.advertiserId, - externalId: "", format: form.format, userId: userId, source: "self_serve", @@ -54,7 +53,6 @@ export function transformNewForm( adSets: form.adSets.map((adSet) => ({ name: adSet.name, billingType: form.billingType, - execution: "per_click", perDay: 1, segments: adSet.segments.map((s) => ({ code: s.code, name: s.name })), oses: adSet.oses, @@ -79,7 +77,7 @@ function transformConversion(conv: Conversion[]) { } export function transformCreative( - creative: Creative, + creative: CreativeInput & { id?: string }, campaign: Pick, ): CreateAdInput { let price: BigNumber; @@ -96,44 +94,18 @@ export function transformCreative( priceType = ConfirmationType.Click; } - return { - webhooks: [], - creativeId: creative.id!, + const createInput: CreateAdInput = { price: price.toString(), priceType: priceType, }; -} - -export function creativeInput( - advertiserId: string, - creative: Creative, - userId?: string, -): CreateNotificationCreativeInput | UpdateNotificationCreativeInput { - const baseNotification = { - advertiserId, - userId, - name: creative.name, - payload: { - title: creative.title, - body: creative.body, - targetUrl: creative.targetUrl, - }, - state: creative.state, - }; if (creative.id) { - return { - ...baseNotification, - creativeId: creative.id, - }; + createInput.creativeId = creative.id; + } else { + createInput.creative = creative; } - return { - ...baseNotification, - type: { - code: "notification_all_v1", - }, - }; + return createInput; } export function editCampaignValues( @@ -148,6 +120,7 @@ export function editCampaignValues( const price = billingType === "cpm" ? rawPrice.multipliedBy(1000) : rawPrice; return { + id: campaign.id, adSets: campaign.adSets.map((adSet) => { const seg = adSet.segments ?? ([] as Segment[]); @@ -159,48 +132,44 @@ export function editCampaignValues( segments: adSet.segments ?? ([] as Segment[]), isNotTargeting: seg.length === 1 && seg[0].code === "Svp7l-zGN", name: adSet.name || adSet.id.split("-")[0], - creatives: creativeList(adSet.ads), + creatives: creativeList(advertiserId, adSet.ads), }; }), advertiserId, - hasPaymentIntent: campaign.hasPaymentIntent ?? false, - creatives: creativeList(ads).map((a) => a.id!), + creatives: creativeList(advertiserId, ads), newCreative: initialCreative, - isCreating: false, price: price.toNumber(), billingType: billingType, validateStart: false, budget: campaign.budget, - currency: campaign.currency, dailyBudget: campaign.dailyBudget, - dailyCap: campaign.dailyCap, endAt: campaign.endAt, format: campaign.format, geoTargets: campaign.geoTargets ?? ([] as GeocodeInput[]), name: campaign.name, - pacingStrategy: campaign.pacingStrategy, startAt: campaign.startAt, state: campaign.state, type: "paid", - stripePaymentId: campaign.stripePaymentId, paymentType: campaign.paymentType, }; } -function creativeList(ads?: AdFragment[] | null): Creative[] { +function creativeList( + advertiserId: string, + ads?: AdFragment[] | null, +): Creative[] { return _.uniqBy( (ads ?? []) .filter((ad) => ad.creative != null && ad.state !== "deleted") .map((ad) => { const c = ad.creative; return { + ...c, + advertiserId, creativeInstanceId: ad.id, id: c.id, name: c.name, - targetUrl: c.payloadNotification!.targetUrl, - title: c.payloadNotification!.title, - body: c.payloadNotification!.body, - targetUrlValidationResult: "", + targetUrlValid: "", state: c.state, }; }), @@ -214,9 +183,7 @@ export function transformEditForm( ): UpdateCampaignInput { return { budget: form.budget, - currency: form.currency, dailyBudget: form.dailyBudget, - dailyCap: form.dailyCap, endAt: form.endAt, id, name: form.name, diff --git a/src/user/views/adsManager/types/index.ts b/src/user/views/adsManager/types/index.ts index e8670198e..f89d6d3f4 100644 --- a/src/user/views/adsManager/types/index.ts +++ b/src/user/views/adsManager/types/index.ts @@ -1,8 +1,4 @@ -import { - CampaignFormat, - CampaignPacingStrategies, - PaymentType, -} from "graphql/types"; +import { CampaignFormat, CreativeInput, PaymentType } from "graphql/types"; import { defaultEndDate, defaultStartDate } from "form/DateFieldHelpers"; import { MIN_PER_CAMPAIGN } from "validation/CampaignSchema"; import { IAdvertiser } from "auth/context/auth.interface"; @@ -10,31 +6,25 @@ import { IAdvertiser } from "auth/context/auth.interface"; export type Billing = "cpm" | "cpc" | "cpv"; export type CampaignForm = { + id?: string; draftId?: string; advertiserId: string; startAt: string; endAt: string; budget: number; validateStart: boolean; - isCreating: boolean; - currency: string; dailyBudget: number; - dailyCap: number; geoTargets: GeoTarget[]; adSets: AdSetForm[]; format: CampaignFormat; newCreative?: Creative; - creatives?: string[]; + creatives?: Creative[]; name: string; state: string; type: "paid"; // this is per click for CPC campaigns, but per thousand views for CPM campaigns price: number; billingType: Billing; - pacingStrategy: CampaignPacingStrategies; - hasPaymentIntent: boolean; - stripePaymentId?: string | null; - radomPaymentId?: string | null; paymentType: PaymentType; }; @@ -69,13 +59,9 @@ export type Segment = { name: string; }; -export type Creative = { +export type Creative = CreativeInput & { id?: string; - name: string; - title: string; - body: string; - targetUrl: string; - targetUrlValidationResult?: string; + targetUrlValid?: string; state?: string; creativeInstanceId?: string; }; @@ -88,9 +74,13 @@ export const initialConversion: Conversion = { export const initialCreative: Creative = { name: "", - title: "", - body: "", - targetUrl: "", + advertiserId: "", + payloadNotification: { + title: "", + targetUrl: "", + body: "", + }, + type: { code: "" }, state: "draft", }; @@ -109,13 +99,10 @@ export const initialCampaign = (advertiser: IAdvertiser): CampaignForm => { startAt: defaultStartDate(), endAt: defaultEndDate(), validateStart: true, - isCreating: false, budget: MIN_PER_CAMPAIGN, - hasPaymentIntent: false, - currency: "USD", dailyBudget: MIN_PER_CAMPAIGN, - dailyCap: 1, geoTargets: [], + newCreative: initialCreative, billingType: "cpm", price: 6, adSets: [ @@ -127,9 +114,7 @@ export const initialCampaign = (advertiser: IAdvertiser): CampaignForm => { name: "", state: "draft", type: "paid", - pacingStrategy: CampaignPacingStrategies.ModelV1, paymentType: advertiser.selfServicePaymentType, - newCreative: initialCreative, creatives: [], }; }; diff --git a/src/user/views/adsManager/views/advanced/components/adSet/AdSetFields.tsx b/src/user/views/adsManager/views/advanced/components/adSet/AdSetFields.tsx index c04e1d10b..01f0f6791 100644 --- a/src/user/views/adsManager/views/advanced/components/adSet/AdSetFields.tsx +++ b/src/user/views/adsManager/views/advanced/components/adSet/AdSetFields.tsx @@ -22,6 +22,7 @@ export function AdSetFields({ isEdit }: Props) { name={`adSets.${current}.name`} label="Ad Set Name" margin="none" + variant="standard" /> diff --git a/src/user/views/adsManager/views/advanced/components/adSet/fields/AdSetAds.tsx b/src/user/views/adsManager/views/advanced/components/adSet/fields/AdSetAds.tsx index 1289451a1..973e001bc 100644 --- a/src/user/views/adsManager/views/advanced/components/adSet/fields/AdSetAds.tsx +++ b/src/user/views/adsManager/views/advanced/components/adSet/fields/AdSetAds.tsx @@ -1,53 +1,24 @@ import { CardContainer } from "components/Card/CardContainer"; -import { Autocomplete, Checkbox, TextField } from "@mui/material"; -import { useRecentlyCreatedAdvertiserCreatives } from "user/hooks/useAdvertiserCreatives"; -import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"; -import CheckBoxIcon from "@mui/icons-material/CheckBox"; -import { useField } from "formik"; -import { Creative } from "user/views/adsManager/types"; -import _ from "lodash"; +import { useRecentlyCreatedAdSetAds } from "user/hooks/useAdvertiserCreatives"; +import { NotificationSelect } from "components/Creatives/NotificationSelect"; +import { Typography } from "@mui/material"; interface Props { index: number; } export function AdSetAds({ index }: Props) { - const creatives = useRecentlyCreatedAdvertiserCreatives(); - const [, meta, helper] = useField(`adSets.${index}.creatives`); + const creatives = useRecentlyCreatedAdSetAds(); return ( - + Select the Ads you would like to include in this ad set. + + + option.name} - renderOption={(props, option, { selected }) => ( -
  • - } - checkedIcon={} - style={{ marginRight: 8 }} - checked={selected} - /> - {option.name} -
  • - )} - renderInput={(params) => ( - - )} - isOptionEqualToValue={(option, value) => option.id === value.id} - value={meta.value} - onChange={(_ev, value) => { - helper.setValue(_.sortBy(value, "name")); - }} - onBlur={() => helper.setTouched(true)} + fieldName={`adSets.${index}.creatives`} />
    ); 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 dc7c56ba6..5a2d498fd 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 @@ -52,9 +52,7 @@ export function BudgetField({ isEdit }: Props) { type="number" InputProps={{ startAdornment: $, - endAdornment: ( - {values.currency} - ), + endAdornment: USD, }} helperText={ errors.budget || errors.dailyBudget diff --git a/src/user/views/adsManager/views/advanced/components/form/EditCampaign.tsx b/src/user/views/adsManager/views/advanced/components/form/EditCampaign.tsx index 632f63332..41caba6d2 100644 --- a/src/user/views/adsManager/views/advanced/components/form/EditCampaign.tsx +++ b/src/user/views/adsManager/views/advanced/components/form/EditCampaign.tsx @@ -12,6 +12,7 @@ import { BaseForm } from "./components/BaseForm"; import { useAdvertiser } from "auth/hooks/queries/useAdvertiser"; import { useCreatePaymentSession } from "checkout/hooks/useCreatePaymentSession"; import { ErrorDetail } from "components/Error/ErrorDetail"; +import { PersistFormValues } from "form/PersistFormValues"; interface Params { campaignId: string; @@ -32,14 +33,15 @@ export function EditCampaign() { fetchPolicy: "cache-and-network", }); + const hasPaymentIntent = initialData?.campaign?.hasPaymentIntent; const [mutation] = useUpdateCampaignMutation({ onCompleted(data) { - if (initialData?.campaign?.hasPaymentIntent) { + if (hasPaymentIntent) { history.push( `/user/main/complete/edit?referenceId=${data.updateCampaign.id}`, ); } else { - createPaymentSession(data.updateCampaign.id); + void createPaymentSession(data.updateCampaign.id); } }, onError() { @@ -73,7 +75,10 @@ export function EditCampaign() { }} validationSchema={CampaignSchema} > - + <> + + + ); 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 1b126b454..e360faf10 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 @@ -11,9 +11,10 @@ import { BudgetSettings } from "user/views/adsManager/views/advanced/components/ interface Props { isEdit: boolean; + hasPaymentIntent?: boolean | null; } -export function BaseForm({ isEdit }: Props) { +export function BaseForm({ isEdit, hasPaymentIntent }: Props) { const { url } = useRouteMatch(); const steps = [ @@ -50,7 +51,12 @@ export function BaseForm({ isEdit }: Props) {
    } + finalComponent={ + + } > {steps.map((s) => ( diff --git a/src/user/views/adsManager/views/advanced/components/form/components/PaymentButton.tsx b/src/user/views/adsManager/views/advanced/components/form/components/PaymentButton.tsx index 8b3e944a2..2ee8ddccb 100644 --- a/src/user/views/adsManager/views/advanced/components/form/components/PaymentButton.tsx +++ b/src/user/views/adsManager/views/advanced/components/form/components/PaymentButton.tsx @@ -1,16 +1,16 @@ import { FormikSubmitButton } from "form/FormikHelpers"; -import { useFormikContext } from "formik"; -import { CampaignForm } from "user/views/adsManager/types"; -export function PaymentButton(props: { isEdit: boolean }) { - const { values } = useFormikContext(); +export function PaymentButton(props: { + isEdit: boolean; + hasPaymentIntent: boolean; +}) { const paymentText = "Make payment & submit for approval"; return ( (); @@ -20,8 +19,6 @@ export function Review() { - - {values.adSets.map((adSet, adSetIdx) => ( - {creatives.length === 0 && ( - No Recently Created Ads - )} - - {creatives.map((c) => ( - - - - ))} - - - ); -} diff --git a/src/user/views/adsManager/views/advanced/components/review/components/CampaignReview.tsx b/src/user/views/adsManager/views/advanced/components/review/components/CampaignReview.tsx index 98d5489db..d113bee0d 100644 --- a/src/user/views/adsManager/views/advanced/components/review/components/CampaignReview.tsx +++ b/src/user/views/adsManager/views/advanced/components/review/components/CampaignReview.tsx @@ -38,7 +38,7 @@ export function CampaignReview({ values, errors }: Props) { /> - schema.shape({ - name: string().label("Creative Name").required("Ad Name is required"), - title: string() - .label("Title") - .max(30, "Maximum 30 Characters") - .required("Ad Title is required"), - body: string() - .label("Body") - .max(60, "Maximum 60 Characters") - .required("Ad Body is required"), - targetUrlValidationResult: string().test({ - test: (value) => _.isEmpty(value), - message: ({ value }) => value, - }), - targetUrl: string() - .label("Target Url") - .required("Ad URL is required") - .matches(NoSpacesRegex, `Ad URL must not contain any whitespace`) - .matches(HttpsRegex, `URL must start with https://`) - .matches( - SimpleUrlRegexp, - `Please enter a valid Ad URL, for example https://brave.com`, - ), - }), - }), + newCreative: CreativeSchema, validateStart: boolean(), dailyBudget: number() .label("Daily Budget")