diff --git a/apps/widget-builder/src/components/buttons/DownloadJsonBtn.tsx b/apps/widget-builder/src/components/buttons/DownloadJsonBtn.tsx index d5f8e51c..308a9f5f 100644 --- a/apps/widget-builder/src/components/buttons/DownloadJsonBtn.tsx +++ b/apps/widget-builder/src/components/buttons/DownloadJsonBtn.tsx @@ -1,3 +1,4 @@ +import DownloadIcon from "@mui/icons-material/Download"; import { Button } from "@mui/material"; import { FC } from "react"; @@ -12,12 +13,13 @@ const DownloadJsonBtn: FC = ({ json }) => ( data-testid="download-button" fullWidth size="large" + color="primary" variant="contained" href={URL.createObjectURL( new Blob([JSON.stringify(json, null, 2)], { type: "application/json" }), )} download={`widget.json`} - sx={{ color: "white" }} + startIcon={} > Download JSON diff --git a/apps/widget-builder/src/components/buttons/IPFSPublishBtn.tsx b/apps/widget-builder/src/components/buttons/IPFSPublishBtn.tsx index b8039385..952d5f6a 100644 --- a/apps/widget-builder/src/components/buttons/IPFSPublishBtn.tsx +++ b/apps/widget-builder/src/components/buttons/IPFSPublishBtn.tsx @@ -1,3 +1,5 @@ +import CloudDoneIcon from "@mui/icons-material/CloudDone"; +import CloudUploadIcon from "@mui/icons-material/CloudUpload"; import { LoadingButton } from "@mui/lab"; import { Stack, Typography } from "@mui/material"; import { SuperfluidButton } from "@superfluid-finance/widget/components"; @@ -14,6 +16,7 @@ const IPFSPublishBtn: FC = ({ json }) => { const { publish, isLoading, ipfsHash } = usePinataIpfs({ pinataMetadata: { name: `${json.productDetails.name}-superfluid-widget` }, }); + const isPublished = !!ipfsHash; return ( @@ -21,13 +24,14 @@ const IPFSPublishBtn: FC = ({ json }) => { data-testid="publish-button" size="large" loading={isLoading} + disabled={isPublished} variant="contained" onClick={() => publish(json)} + startIcon={isPublished ? : } > - Publish + {isPublished ? "Published to IPFS" : "Publish to IPFS"} - - {ipfsHash && ( + {isPublished && ( = ({ setValue, }) => ( setIsOpen(false)} keepMounted={true} diff --git a/apps/widget-builder/src/components/export-editor/ExportEditor.tsx b/apps/widget-builder/src/components/export-editor/ExportEditor.tsx index 29cef78a..1f83eeba 100644 --- a/apps/widget-builder/src/components/export-editor/ExportEditor.tsx +++ b/apps/widget-builder/src/components/export-editor/ExportEditor.tsx @@ -1,11 +1,4 @@ -import { - Box, - Divider, - MenuItem, - Select, - Stack, - Typography, -} from "@mui/material"; +import { Box, MenuItem, Select, Stack, Typography } from "@mui/material"; import { FC, useMemo, useState } from "react"; import { useFormContext } from "react-hook-form"; @@ -76,10 +69,13 @@ const ExportEditor: FC = () => { useState("ipfs"); return ( - + + + Checkout Export + { )} - + How does it work? - + {selectedExportOption === "ipfs" ? "You’ll create a hosted link to your checkout which you can embed in your CTAs." : selectedExportOption === "json" @@ -118,17 +114,15 @@ const ExportEditor: FC = () => { {switchExportOption(selectedExportOption, json)} - - - - + + Do you have more questions? - + We’ll show you how your business can benefit from using our checkout. Book a Demo - + ); }; diff --git a/apps/widget-builder/src/components/form/InputWrapper.tsx b/apps/widget-builder/src/components/form/InputWrapper.tsx index 8a442066..744549fe 100644 --- a/apps/widget-builder/src/components/form/InputWrapper.tsx +++ b/apps/widget-builder/src/components/form/InputWrapper.tsx @@ -1,4 +1,4 @@ -import InfoIcon from "@mui/icons-material/Info"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import { FormControl, FormHelperText, @@ -6,6 +6,7 @@ import { Stack, SxProps, Tooltip, + Typography, useTheme, } from "@mui/material"; import { FC, PropsWithChildren, useId } from "react"; @@ -19,7 +20,7 @@ export const InputInfo: FC = ({ tooltip }) => { return ( - + ); }; @@ -30,7 +31,9 @@ interface InputWrapperProps { tooltip?: string; sx?: SxProps; helperText?: string; - children: (inputId: string) => PropsWithChildren["children"]; + optional?: boolean; + error?: boolean; + children: (inputId: string, error?: boolean) => PropsWithChildren["children"]; } const InputWrapper: FC = ({ @@ -39,23 +42,37 @@ const InputWrapper: FC = ({ sx = {}, helperText, children, + optional, + error, ...props }) => { const generatedId = useId(); const inputId = props.id ?? generatedId; const labelId = `label-${inputId}`; return ( - + {!!title && ( {title} + {!!optional && ( + + (optional) + + )} )} {!!tooltip && } - {children(inputId)} - {!!helperText && {helperText}} + {children(inputId, error)} + {!!helperText && ( + {helperText} + )} ); }; diff --git a/apps/widget-builder/src/components/image-select/ImageSelect.tsx b/apps/widget-builder/src/components/image-select/ImageSelect.tsx index 1487c637..6758fd49 100644 --- a/apps/widget-builder/src/components/image-select/ImageSelect.tsx +++ b/apps/widget-builder/src/components/image-select/ImageSelect.tsx @@ -1,24 +1,17 @@ import AddIcon from "@mui/icons-material/Add"; import CancelIcon from "@mui/icons-material/Cancel"; -import { - Box, - Button, - IconButton, - Stack, - Typography, - useTheme, -} from "@mui/material"; +import { Box, Button, IconButton, Stack, useTheme } from "@mui/material"; import { ChangeEvent, FC, useRef } from "react"; type ImageSelectProps = { - label: string; + id: string; imageSrc?: string; onClick: (file: File) => void; onRemove: () => void; }; const ImageSelect: FC = ({ - label, + id, imageSrc, onClick, onRemove, @@ -39,19 +32,13 @@ const ImageSelect: FC = ({ return ( - - - {label} - - {imageSrc && ( + {imageSrc && ( + - )} - + + )} {imageSrc ? ( = ({ data-testid="file-upload-field" hidden type="file" - name={label} + name={id} ref={inputRef} onChange={handleFileUpload} /> @@ -92,16 +79,6 @@ const ImageSelect: FC = ({ height: 90, }} > - - Optional - { + const { control, watch } = useFormContext(); + + const { fields, append, remove } = useFieldArray({ + control, + name: "paymentDetails.paymentOptions", // unique name for your Field Array + }); + + const [paymentOptions] = watch(["paymentDetails.paymentOptions"]); + + const [isAdding, setIsAdding] = useState(false); + const [addCount, setAddCount] = useState(0); + + const handleOpen = () => { + setIsAdding(true); + }; + + const handleClose = () => { + setIsAdding(false); + }; + + const { setDemoPaymentDetails } = useDemoMode(); + const handleDemo = useCallback(() => { + setAddCount((x) => x + 1); + setDemoPaymentDetails(); + }, []); + + return ( + <> + + + + Checkout Payment Details + + + Enter your preferred payment options to start receiving ongoing + real-time payments powered by the Superfluid Protocol. + + + + + + + Payment Options + + ({paymentOptions.length}) + + + + + + + + + + {paymentOptions.length ? ( + paymentOptions + .map( + ( + { + superToken, + chainId, + transferAmountEther, + flowRate, + receiverAddress, + }, + i, + ) => ( + + + { + remove(params); + setAddCount(0); + }} + /> + + + ), + ) + .reverse() + ) : ( + + {"You haven't added any payment options yet."} Add your first + one or{" "} + + replace with demo data + + . + + )} + + + + + + + Add Payment Option + + + + + + + ( + { + setAddCount((x) => x + 1); + append(props); + handleClose(); + }} + onDiscard={() => { + handleClose(); + setAddCount((x) => x + 1); + }} + /> + )} + /> + + + + + + + + ); +}; + +export default ProductEditor; diff --git a/apps/widget-builder/src/components/payment-option-view/PaymentOptionView.tsx b/apps/widget-builder/src/components/payment-option-view/PaymentOptionView.tsx index c1635fe8..412815a2 100644 --- a/apps/widget-builder/src/components/payment-option-view/PaymentOptionView.tsx +++ b/apps/widget-builder/src/components/payment-option-view/PaymentOptionView.tsx @@ -1,9 +1,14 @@ -import CloseIcon from "@mui/icons-material/Close"; import { Button, - Paper, + Card, + CardActions, + CardContent, + Divider, Stack, + styled, Tooltip, + tooltipClasses, + TooltipProps, Typography, useTheme, } from "@mui/material"; @@ -15,6 +20,7 @@ import { import superTokenList from "@superfluid-finance/widget/tokenlist"; import Image from "next/image"; import { FC, ReactNode, useMemo } from "react"; +import { getAddress } from "viem"; import NetworkAvatar from "../NetworkAvatar"; @@ -23,6 +29,14 @@ type PaymentOptionRowProps = { value: ReactNode; }; +const NoMaxWidthTooltip = styled(({ className, ...props }: TooltipProps) => ( + +))({ + [`& .${tooltipClasses.tooltip}`]: { + maxWidth: "none", + }, +}); + const PaymentOptionRow: FC = ({ label, value }) => { const theme = useTheme(); return ( @@ -33,14 +47,7 @@ const PaymentOptionRow: FC = ({ label, value }) => { direction="row" sx={{ width: "100%", justifyContent: "space-between" }} > - + {label} {value} @@ -51,7 +58,7 @@ const PaymentOptionRow: FC = ({ label, value }) => { type PaymentOptionViewProps = { superToken: { address: `0x${string}` }; upfrontPaymentAmountEther?: string; - flowRate: FlowRate; + flowRate: FlowRate | undefined; receiverAddress: `0x${string}`; chainId: ChainId; index: number; @@ -62,135 +69,127 @@ const PaymentOptionView: FC = ({ superToken, upfrontPaymentAmountEther, flowRate, - receiverAddress, + receiverAddress: receiverAddress_, chainId, index, remove, }) => { - const theme = useTheme(); - const network = supportedNetworks.find((n) => n.id === chainId)!; - const token = Object.values(superTokenList.tokens).find( - (token) => token.address === superToken.address, + const network = useMemo( + () => supportedNetworks.find((n) => n.id === chainId)!, + [chainId], + ); + const receiverAddress = useMemo( + () => getAddress(receiverAddress_), + [receiverAddress_], + ); + + const token = useMemo( + () => + Object.values(superTokenList.tokens).find( + (token) => + token.address.toLowerCase() === superToken.address.toLowerCase(), + ), + [superTokenList, superToken.address], ); const flowRateValue = useMemo(() => { - if (!flowRate) return "Custom amount"; + if (!flowRate) return "User-defined"; return `${flowRate.amountEther} ${token?.symbol ?? "x"}/${flowRate.period}`; }, [flowRate, token]); return ( - - - - - {network?.name} - - } - /> - - {token?.logoURI && ( - - )} - {token?.name} - - } - /> - - {upfrontPaymentAmountEther && ( + + + + + {network?.name} + + } /> - )} - - - {`${receiverAddress.substring( - 0, - 6, - )}...${receiverAddress.substring( - receiverAddress.length - 4, - receiverAddress.length, + + {token?.logoURI && ( + )} + {token?.name} + + } + /> + + {upfrontPaymentAmountEther && ( + + )} + + + {`${receiverAddress.substring( + 0, + 6, + )}...${receiverAddress.substring( + receiverAddress.length - 4, + receiverAddress.length, + )} `} - - - } - /> - - + + + } + /> + + + - {/* */} - + ); }; diff --git a/apps/widget-builder/src/components/product-editor/ProductEditor.tsx b/apps/widget-builder/src/components/product-editor/ProductEditor.tsx index b3d0b138..0d27dd12 100644 --- a/apps/widget-builder/src/components/product-editor/ProductEditor.tsx +++ b/apps/widget-builder/src/components/product-editor/ProductEditor.tsx @@ -1,11 +1,11 @@ -import { Divider, Stack, TextField, Typography } from "@mui/material"; +import AutoFixHighIcon from "@mui/icons-material/AutoFixHigh"; +import { Box, Fab, Stack, TextField, Tooltip, Typography } from "@mui/material"; import { FC } from "react"; import { Controller, useFieldArray, useFormContext } from "react-hook-form"; -import theme from "../../theme"; +import useDemoMode from "../../hooks/useDemoMode"; import InputWrapper from "../form/InputWrapper"; -import PaymentOptionView from "../payment-option-view/PaymentOptionView"; -import SelectPaymentOption from "../select-payment-option/SelectPaymentOption"; +import ImageSelect from "../image-select/ImageSelect"; import { WidgetProps } from "../widget-preview/WidgetPreview"; const ProductEditor: FC = () => { @@ -17,17 +17,25 @@ const ProductEditor: FC = () => { }); const [paymentOptions] = watch(["paymentDetails.paymentOptions"]); + const { setDemoProductDetails } = useDemoMode(); return ( - - - Payment Configuration - + <> + + + + Checkout Product Details + + + Define the product you want to receive ongoing real-time payments + for. + + ( - + {(id) => ( { control={control} name="productDetails.description" render={({ field: { value, onChange } }) => ( - + {(id) => ( { )} /> - - - - Add Payment Options } + name="productDetails.imageURI" + render={({ field: { value, onChange } }) => ( + + {(id) => ( + onChange(URL.createObjectURL(file))} + onRemove={() => onChange("")} + imageSrc={value} + /> + )} + + )} /> - - - - - - Payment Details Summary - - Added: {paymentOptions.length} - - - - - {paymentOptions.length ? ( - paymentOptions.map( - ( - { - superToken, - chainId, - transferAmountEther, - flowRate, - receiverAddress, - }, - i, - ) => ( - - ), - ) - ) : ( - - - None - - )} - - - + + + + + + ); }; diff --git a/apps/widget-builder/src/components/select-payment-option/SelectPaymentOption.tsx b/apps/widget-builder/src/components/select-payment-option/SelectPaymentOption.tsx index c5990a63..47d1e12e 100644 --- a/apps/widget-builder/src/components/select-payment-option/SelectPaymentOption.tsx +++ b/apps/widget-builder/src/components/select-payment-option/SelectPaymentOption.tsx @@ -2,14 +2,17 @@ import { Autocomplete, Avatar, Button, - Chip, Collapse, + DialogActions, + DialogContent, + Divider, FormControlLabel, FormGroup, InputAdornment, ListItem, ListItemAvatar, ListItemText, + ListSubheader, MenuItem, Select, SelectChangeEvent, @@ -22,6 +25,8 @@ import { import { ChainId, NetworkAssetInfo, + PaymentOption, + paymentOptionSchema, supportedNetworks, TimePeriod, timePeriods, @@ -32,21 +37,26 @@ import tokenList, { import { ChangeEvent, FC, useEffect, useMemo, useState } from "react"; import { UseFieldArrayAppend } from "react-hook-form"; import { Chain } from "wagmi"; +import { ZodError } from "zod"; import InputWrapper, { InputInfo } from "../form/InputWrapper"; import NetworkAvatar from "../NetworkAvatar"; import { WidgetProps } from "../widget-preview/WidgetPreview"; -export type PaymentOption = { +export type PaymentOptionWithSuperTokenAndNetwork = { network: NetworkAssetInfo; superToken: SuperTokenInfo; }; type PaymentOptionSelectorProps = { onAdd: UseFieldArrayAppend; + onDiscard: () => void; }; -const SelectPaymentOption: FC = ({ onAdd }) => { +const SelectPaymentOption: FC = ({ + onAdd, + onDiscard, +}) => { const [receiver, setReceiver] = useState<`0x${string}` | "">(""); const [selectedNetwork, setSelectedNetwork] = useState(null); const [selectedToken, setSelectedToken] = useState( @@ -86,38 +96,41 @@ const SelectPaymentOption: FC = ({ onAdd }) => { } }; + const [errors, setErrors] = useState | null>(null); + const handleAdd = () => { - if (!selectedToken) { - return; - } + setErrors(null); - const network = supportedNetworks.find( - (n) => n.id === selectedToken.chainId, - ); + const thePaymentOption: Partial = { + receiverAddress: receiver as `0x${string}`, + superToken: selectedToken + ? { + address: selectedToken.address as `0x${string}`, + } + : undefined, + chainId: selectedToken ? (selectedToken.chainId as ChainId) : undefined, + ...(!isCustomAmount + ? { + ...(showUpfrontPayment + ? { + transferAmountEther: upfrontPaymentAmount + ? upfrontPaymentAmount + : "0", + } + : {}), + flowRate: { + amountEther: flowRateAmount ? flowRateAmount : "0", + period: flowRateInterval, + }, + } + : {}), + }; - if (network && receiver) { - onAdd({ - receiverAddress: receiver, - superToken: { - address: selectedToken.address as `0x${string}`, - }, - chainId: selectedToken.chainId as ChainId, - ...(!isCustomAmount - ? { - ...(showUpfrontPayment - ? { - transferAmountEther: upfrontPaymentAmount - ? upfrontPaymentAmount - : "0", - } - : {}), - flowRate: { - amountEther: flowRateAmount ? flowRateAmount : "0", - period: flowRateInterval, - }, - } - : {}), - }); + const validationResult = paymentOptionSchema.safeParse(thePaymentOption); + if (validationResult.success) { + onAdd(validationResult.data); + } else { + setErrors(validationResult.error); } }; @@ -150,277 +163,313 @@ const SelectPaymentOption: FC = ({ onAdd }) => { const [upfrontPaymentAmount, setUpfrontPaymentAmount] = useState< `${number}` | "" >(""); - const onShowUpfrontPaymentChanged = (_e: ChangeEvent, checked: boolean) => - setShowUpfrontPayment(checked); + const onShowUpfrontPaymentChanged = (_e: ChangeEvent, checked: boolean) => { + if (checked) { + setShowUpfrontPayment(true); + setUpfrontPaymentAmount(flowRateAmount); + } else { + setShowUpfrontPayment(false); + setUpfrontPaymentAmount(""); + } + }; return ( - - - {(id) => ( - - Fixed rate - User-defined rate - - )} - - - - - {(id) => ( - - )} - - - - {(id) => ( - setSelectedToken(value!)} - id={id} - options={autoCompleteTokenOptions} - getOptionLabel={(token) => token.symbol} - componentsProps={{ - popper: { - placement: "bottom-end", - }, - }} - renderOption={(props, option) => ( - - - {option.logoURI && ( - - )} - - - - )} - renderInput={(params) => ( - - ), - }} - /> - )} - /> - )} - - - - - + <> + + {(id) => ( - - - setFlowRateAmount(target.value as `${number}`) - } - InputProps={{ - sx: { - borderTopRightRadius: 0, - borderBottomRightRadius: 0, - }, - endAdornment: ( - - {selectedToken?.symbol} - - ), - }} - /> - - + Testnets + {filteredNetworks + .filter((x) => x.testnet) + .map((network) => ( + + + + {network.name} + + + ))} + )} - - - + + + {(id, error) => ( + + setReceiver(target.value as `0x${string}`) } - label="Charge upfront payment amount" /> - - - + )} + - - - - {(id) => ( + + {(id, error) => ( + setSelectedToken(value!)} + id={id} + options={autoCompleteTokenOptions} + getOptionLabel={(token) => token.symbol} + componentsProps={{ + popper: { + placement: "bottom-end", + }, + }} + renderOption={(props, option) => ( + + + {option.logoURI && ( + + )} + + + + )} + renderInput={(params) => ( - setUpfrontPaymentAmount(target.value as `${number}`) - } + {...params} + error={error} InputProps={{ - endAdornment: ( - - {selectedToken?.symbol} - + ...params.InputProps, + startAdornment: selectedToken?.logoURI && ( + ), }} /> )} - - - - - + /> + )} + - - {(id) => ( - - setReceiver(target.value as `0x${string}`) + - )} - + > + {(id) => ( + + Fixed rate + User-defined rate + + )} + - - + + + + {(id, error) => ( + + + setFlowRateAmount(target.value as `${number}`) + } + InputProps={{ + sx: { + borderTopRightRadius: 0, + borderBottomRightRadius: 0, + }, + endAdornment: ( + + {selectedToken?.symbol} + + ), + }} + /> + + + )} + + + + + } + label="Charge upfront payment amount" + /> + + + + + + + + {(id, error) => ( + + setUpfrontPaymentAmount(target.value as `${number}`) + } + InputProps={{ + endAdornment: ( + + {selectedToken?.symbol} + + ), + }} + /> + )} + + + + + + + + + + + + + + + ); }; diff --git a/apps/widget-builder/src/components/ui-editor/UiEditor.tsx b/apps/widget-builder/src/components/ui-editor/UiEditor.tsx index d25311e3..c5ae62b9 100644 --- a/apps/widget-builder/src/components/ui-editor/UiEditor.tsx +++ b/apps/widget-builder/src/components/ui-editor/UiEditor.tsx @@ -1,5 +1,8 @@ +import AutoFixHighIcon from "@mui/icons-material/AutoFixHigh"; import { Autocomplete, + Box, + Fab, FormControlLabel, Slider, Stack, @@ -7,15 +10,16 @@ import { TextField, ToggleButton, ToggleButtonGroup, + Tooltip, Typography, } from "@mui/material"; import { MuiColorInput } from "mui-color-input"; import { FC } from "react"; import { Controller, useFormContext } from "react-hook-form"; +import useDemoMode from "../../hooks/useDemoMode"; import useFontOptions from "../../hooks/useFontOptions"; import InputWrapper from "../form/InputWrapper"; -import ImageSelect from "../image-select/ImageSelect"; import { WidgetProps } from "../widget-preview/WidgetPreview"; const UiEditor: FC = () => { @@ -24,126 +28,204 @@ const UiEditor: FC = () => { const [displaySettings] = watch(["displaySettings"]); const fontOptions = useFontOptions(); + const { setDemoStyling } = useDemoMode(); + return ( - - + <> + + + + Checkout Widget Styling + + + You are free to customize the look and feel of the checkout widget. + + + ( - onChange(URL.createObjectURL(file))} - onRemove={() => onChange("")} - imageSrc={value} - /> + + {(id) => ( + onChange(value)} + sx={{ + borderTopRightRadius: 0, + borderBottomRightRadius: 0, + }} + > + + Inline + + + Dialog + + + Drawer + + + Full-screen + + + )} + )} /> - {/* ( - onChange({ target: { value: file } })} - onRemove={() => onChange({ target: { value: "" } })} - imageSrc={value ? URL.createObjectURL(value) : ""} + } + label={ + {`Dark mode: ${value ? "on" : "off"}`} + } /> )} - /> */} - - ( - } - label={ - {`Dark mode: ${value ? "on" : "off"}`} - } - /> - )} - /> + /> + ( + + {(id) => ( + onChange(x as number)} + /> + )} + + )} + /> - ( - - {(id) => ( - onChange(x as number)} - /> - )} - - )} - /> + ( + + {(id) => ( + onChange(x as number)} + /> + )} + + )} + /> - ( - - {(id) => ( - onChange(x as number)} - /> + ( + + {(id) => ( + onChange(x as number)} + /> + )} + + )} + /> + + + ( + + {(id) => ( + onChange(x as `#{string}`)} + /> + )} + )} - - )} - /> + /> - ( - - {(id) => ( - onChange(x as number)} - /> + ( + + {(id) => ( + onChange(x as `#{string}`)} + /> + )} + )} - - )} - /> + /> + - ( - + {(id) => ( - onChange(x as `#{string}`)} + loading={fontOptions.length === 0} + disablePortal + options={fontOptions} + isOptionEqualToValue={(option, value) => + option.family === value.family + } + onChange={(_, value) => onChange(value)} + getOptionLabel={(option) => + `${option.family}, ${option.category}` + } + fullWidth + renderInput={(params) => } /> )} @@ -152,88 +234,55 @@ const UiEditor: FC = () => { ( - + {(id) => ( - onChange(x as `#{string}`)} - /> + exclusive + onChange={(_, x: "vertical" | "horizontal") => onChange(x)} + sx={{ + borderTopRightRadius: 0, + borderBottomRightRadius: 0, + }} + > + + Vertical + + + Horizontal + + )} )} /> - - ( - - {(id) => ( - - option.family === value.family - } - onChange={(_, value) => onChange(value)} - getOptionLabel={(option) => - `${option.family}, ${option.category}` - } - fullWidth - renderInput={(params) => } - /> - )} - - )} - /> - - ( - - {(id) => ( - onChange(x)} - sx={{ - borderTopRightRadius: 0, - borderBottomRightRadius: 0, - }} - > - - Vertical - - - Horizontal - - - )} - - )} - /> - + + + + + + ); }; diff --git a/apps/widget-builder/src/components/widget-preview/WidgetPreview.tsx b/apps/widget-builder/src/components/widget-preview/WidgetPreview.tsx index fa49287b..d39bb3a2 100644 --- a/apps/widget-builder/src/components/widget-preview/WidgetPreview.tsx +++ b/apps/widget-builder/src/components/widget-preview/WidgetPreview.tsx @@ -1,4 +1,4 @@ -import { Button, colors, SelectChangeEvent, ThemeOptions } from "@mui/material"; +import { colors, Fab, SelectChangeEvent, ThemeOptions } from "@mui/material"; import SuperfluidWidget, { PaymentDetails, ProductDetails, @@ -115,7 +115,11 @@ const switchLayout = ( stepper={{ orientation: stepperOrientation }} > {({ openModal }) => ( - + openModal()} + >{`Open Checkout in ${layout.toUpperCase()}`} )} ); diff --git a/apps/widget-builder/src/hooks/useDemoMode.ts b/apps/widget-builder/src/hooks/useDemoMode.ts index 79875a98..eb50332f 100644 --- a/apps/widget-builder/src/hooks/useDemoMode.ts +++ b/apps/widget-builder/src/hooks/useDemoMode.ts @@ -4,7 +4,8 @@ import { ProductDetails, supportedNetwork, } from "@superfluid-finance/widget"; -import { useMemo, useState } from "react"; +import { useCallback } from "react"; +import { useFormContext } from "react-hook-form"; import { DisplaySettings, @@ -12,12 +13,6 @@ import { WidgetProps, } from "../components/widget-preview/WidgetPreview"; -const demoProductDetails: ProductDetails = { - name: `${faker.commerce.productName()}`, - description: `${faker.commerce.productDescription()}`, - imageURI: "https://picsum.photos/200/200", -}; - const defaultProductDetails: ProductDetails = { name: "", description: "", @@ -131,7 +126,7 @@ const defaultPaymentDetails: PaymentDetails = { const type: Layout = "page"; -const displaySettings: DisplaySettings = { +const defaultDisplaySettings: DisplaySettings = { darkMode: false, containerRadius: 20, buttonRadius: 10, @@ -145,36 +140,47 @@ const displaySettings: DisplaySettings = { stepperOrientation: "vertical", }; -const useDemoMode = () => { - const [demoMode, setDemoMode] = useState(false); +export const defaultWidgetProps: WidgetProps = { + productDetails: defaultProductDetails, + paymentDetails: defaultPaymentDetails, + type, + displaySettings: defaultDisplaySettings, +}; - const toggleDemoMode = () => { - setDemoMode(!demoMode); - }; +const useDemoMode = () => { + const { setValue } = useFormContext(); - const widgetProps = useMemo( - () => ({ - ...(demoMode - ? { - productDetails: demoProductDetails, - paymentDetails: demoPaymentDetails, - type, - displaySettings, - } - : { - productDetails: defaultProductDetails, - paymentDetails: defaultPaymentDetails, - type, - displaySettings, - }), - }), - [demoMode], + const setDemoPaymentDetails = useCallback( + () => setValue("paymentDetails", demoPaymentDetails), + [setValue], ); + const setDemoProductDetails = useCallback(() => { + const demoProductDetails: ProductDetails = { + name: `${faker.commerce.productName()}`, + description: `${faker.commerce.productDescription()}`, + imageURI: "https://picsum.photos/200/200", + }; + setValue("productDetails", demoProductDetails); + }, [setValue]); + + const setDemoStyling = useCallback(() => { + const demoStyling: DisplaySettings = { + ...defaultDisplaySettings, + darkMode: faker.datatype.boolean(), + primaryColor: faker.color.rgb() as `#${string}`, + secondaryColor: faker.color.rgb() as `#${string}`, + containerRadius: faker.number.int({ min: 0, max: 50 }), + buttonRadius: faker.number.int({ min: 0, max: 25 }), + inputRadius: faker.number.int({ min: 0, max: 25 }), + }; + setValue("displaySettings", demoStyling); + }, [setValue]); + return { - demoMode, - toggleDemoMode, - widgetProps, + setDemoPaymentDetails, + setDemoProductDetails, + setDemoStyling, }; }; diff --git a/apps/widget-builder/src/pages/builder.tsx b/apps/widget-builder/src/pages/builder.tsx index 6eb585ae..45f25938 100644 --- a/apps/widget-builder/src/pages/builder.tsx +++ b/apps/widget-builder/src/pages/builder.tsx @@ -1,47 +1,49 @@ import CodeIcon from "@mui/icons-material/Code"; -import FullscreenIcon from "@mui/icons-material/Fullscreen"; -import ViewSidebarIcon from "@mui/icons-material/ViewSidebar"; -import WebIcon from "@mui/icons-material/Web"; -import WebAssetIcon from "@mui/icons-material/WebAsset"; +import KeyboardArrowLeftIcon from "@mui/icons-material/KeyboardArrowLeft"; +import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight"; import { TabContext, TabList, TabPanel } from "@mui/lab"; import { + AppBar, Box, Button, Drawer, - FormControlLabel, + MobileStepper, + Paper, Stack, - Switch, Tab, - ToggleButton, - ToggleButtonGroup, + Toolbar, Typography, - useTheme, } from "@mui/material"; import { useState } from "react"; -import { Controller, FormProvider, useForm } from "react-hook-form"; +import { FormProvider, useForm } from "react-hook-form"; import ConfigEditorDrawer from "../components/config-editor/ConfigEditorDrawer"; import ExportEditor from "../components/export-editor/ExportEditor"; +import PaymentEditor from "../components/payment-editor/PaymentEditor"; import ProductEditor from "../components/product-editor/ProductEditor"; import TermsAndPrivacy from "../components/terms-and-privacy/TermsAndPrivacy"; import UiEditor from "../components/ui-editor/UiEditor"; import WidgetPreview, { WidgetProps, } from "../components/widget-preview/WidgetPreview"; -import useDemoMode from "../hooks/useDemoMode"; +import { defaultWidgetProps } from "../hooks/useDemoMode"; -const drawerWidth = "480px"; +export const drawerWidth = "480px"; export default function Builder() { - const theme = useTheme(); - const [activeTab, setActiveTab] = useState<"ui" | "product" | "export">( - "product", - ); - - const { widgetProps, demoMode, toggleDemoMode } = useDemoMode(); + const [activeStep, setActiveStep] = useState(0); + const stepCount = 4; + const handleNext = () => { + setActiveStep((prevActiveStep) => + Math.min(prevActiveStep + 1, stepCount - 1), + ); + }; + const handleBack = () => { + setActiveStep((prevActiveStep) => Math.max(prevActiveStep - 1, 0)); + }; const formMethods = useForm({ - values: widgetProps, + values: defaultWidgetProps, }); const { watch, control, getValues, setValue } = formMethods; @@ -69,45 +71,93 @@ export default function Builder() { }, }} > - - - Widget Builder - - } - label={Demo} - /> - + + + + + Checkout Builder + + + + setActiveStep(Number(value))} + > + + + + + + + - - setActiveTab(value)} sx={{ px: 2 }}> - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + Next + + + } + backButton={ + + } + /> + - - ( - onChange(value)} - sx={{ - borderTopRightRadius: 0, - borderBottomRightRadius: 0, - }} - > - - - - - - - - - - - - - - )} - /> - diff --git a/examples/b2b-service-demo/src/components/BookModal/BookModal.tsx b/examples/b2b-service-demo/src/components/BookModal/BookModal.tsx index 7407cf41..e9b53409 100644 --- a/examples/b2b-service-demo/src/components/BookModal/BookModal.tsx +++ b/examples/b2b-service-demo/src/components/BookModal/BookModal.tsx @@ -23,7 +23,7 @@ const BookModal: FC = ({ show, onClose }) => { href="https://checkout-builder.superfluid.finance/" target="_blank" > - Try the Widget Builder + Try the Checkout Builder = ({ show, onClose }) => { href="https://checkout-builder.superfluid.finance/" target="_blank" > - Try the Widget Builder + Try the Checkout Builder = ({ show, onClose }) => { href="https://checkout-builder.superfluid.finance/" target="_blank" > - Try the Widget Builder + Try the Checkout Builder