From 5ad02756d94824d1e48d94e967687a4fae1fc3fb Mon Sep 17 00:00:00 2001 From: blushi Date: Wed, 18 Sep 2024 15:46:48 +0200 Subject: [PATCH 01/10] feat: add prev/next btn to PaymentInfo and use stripe key --- .../ChooseCreditsForm/ChooseCreditsForm.tsx | 4 ++-- .../PaymentInfoForm/PaymentInfoForm.tsx | 15 +++++++++++- .../MultiStepTemplate/MultiStep.context.tsx | 9 ++++++-- .../MultiStepTemplate/MultiStepTemplate.tsx | 2 ++ web-marketplace/src/hooks/index.ts | 2 +- .../{useLocalStorage.ts => useStorage.ts} | 23 +++++++++++-------- .../src/pages/BuyCredits/BuyCredits.Form.tsx | 1 + .../pages/BuyCredits/BuyCredits.constants.ts | 3 +++ .../src/pages/BuyCredits/BuyCredits.tsx | 1 + 9 files changed, 44 insertions(+), 16 deletions(-) rename web-marketplace/src/hooks/{useLocalStorage.ts => useStorage.ts} (68%) diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx index bab4121ff4..b9528e0275 100644 --- a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx @@ -18,7 +18,7 @@ import { Loading } from 'web-components/src/components/loading'; import { PrevNextButtons } from 'web-components/src/components/molecules/PrevNextButtons/PrevNextButtons'; import { UseStateSetter } from 'web-components/src/types/react/useState'; -import { PAYMENT_OPTIONS } from 'pages/BuyCredits/BuyCredits.constants'; +import { NEXT, PAYMENT_OPTIONS } from 'pages/BuyCredits/BuyCredits.constants'; import { PaymentOptionsType } from 'pages/BuyCredits/BuyCredits.types'; import { UISellOrderInfo } from 'pages/Projects/AllProjects/AllProjects.types'; import { Currency } from 'components/molecules/CreditsAmount/CreditsAmount.types'; @@ -231,7 +231,7 @@ export function ChooseCreditsForm({
{ navigate(projectHref); }} diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx index 6a820cfeda..b10a5e04bd 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx @@ -1,14 +1,17 @@ import { useMemo } from 'react'; import { useFormState } from 'react-hook-form'; +import { useLingui } from '@lingui/react'; import { Elements } from '@stripe/react-stripe-js'; import { loadStripe, StripeElementsOptionsMode } from '@stripe/stripe-js'; +import { PrevNextButtons } from 'web-components/src/components/molecules/PrevNextButtons/PrevNextButtons'; import { defaultFontFamily } from 'web-components/src/theme/muiTheme'; -import { PAYMENT_OPTIONS } from 'pages/BuyCredits/BuyCredits.constants'; +import { NEXT, PAYMENT_OPTIONS } from 'pages/BuyCredits/BuyCredits.constants'; import { PaymentOptionsType } from 'pages/BuyCredits/BuyCredits.types'; import Form from 'components/molecules/Form/Form'; import { useZodForm } from 'components/molecules/Form/hook/useZodForm'; +import { useMultiStep } from 'components/templates/MultiStepTemplate'; import { CustomerInfo, @@ -43,6 +46,9 @@ export const PaymentInfoForm = ({ currency, retiring, }: PaymentInfoFormProps) => { + const { _ } = useLingui(); + const { handleBack } = useMultiStep(); + const form = useZodForm({ schema: paymentInfoFormSchema(paymentOption), defaultValues: { @@ -126,6 +132,13 @@ export const PaymentInfoForm = ({ /> )} +
+ +
); diff --git a/web-marketplace/src/components/templates/MultiStepTemplate/MultiStep.context.tsx b/web-marketplace/src/components/templates/MultiStepTemplate/MultiStep.context.tsx index 6828cd9324..cd059252cb 100644 --- a/web-marketplace/src/components/templates/MultiStepTemplate/MultiStep.context.tsx +++ b/web-marketplace/src/components/templates/MultiStepTemplate/MultiStep.context.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { msg } from '@lingui/macro'; import { useLingui } from '@lingui/react'; -import { useLocalStorage } from 'hooks'; +import { useStorage } from 'hooks'; // TODO - persistence alternatives: component / localstorage / db // Instead of directly using the local storage hook here, we should use an @@ -83,6 +83,7 @@ export type ProviderProps = { initialValues: T; steps: Step[]; children?: JSX.Element | JSX.Element[]; + useLocalStorage?: boolean; }; export function MultiStepProvider({ @@ -90,13 +91,17 @@ export function MultiStepProvider({ initialValues, steps, children, + useLocalStorage = true, }: React.PropsWithChildren>): JSX.Element { // we don't pass initialValues to localStorage // to avoid persist the initial empty data structure. // So initially, data (from storage) is `undefined` or // previously persisted data. // If undefined, then we return the initialValues - const { data, saveData, removeData } = useLocalStorage>(formId); + const { data, saveData, removeData } = useStorage>( + formId, + useLocalStorage, + ); const maxAllowedStep = data?.maxAllowedStep || 0; diff --git a/web-marketplace/src/components/templates/MultiStepTemplate/MultiStepTemplate.tsx b/web-marketplace/src/components/templates/MultiStepTemplate/MultiStepTemplate.tsx index e77072e78c..66bc71354e 100644 --- a/web-marketplace/src/components/templates/MultiStepTemplate/MultiStepTemplate.tsx +++ b/web-marketplace/src/components/templates/MultiStepTemplate/MultiStepTemplate.tsx @@ -10,12 +10,14 @@ export function MultiStepTemplate({ steps, initialValues, children, + useLocalStorage, }: MultiStepProps): JSX.Element { return ( {children} diff --git a/web-marketplace/src/hooks/index.ts b/web-marketplace/src/hooks/index.ts index 298e8c5420..5fa8fec3b0 100644 --- a/web-marketplace/src/hooks/index.ts +++ b/web-marketplace/src/hooks/index.ts @@ -1,7 +1,7 @@ export { default as useBasketTokens } from './useBasketTokens'; export { default as useEcocreditQuery } from './useEcocreditQuery'; export { default as useEcocredits } from './useEcocredits'; -export { default as useLocalStorage } from './useLocalStorage'; export { default as useMsgClient } from './useMsgClient'; export { default as useQueryBalance } from './useQueryBalance'; export { default as useQueryDenomMetadata } from './useQueryDenomMetadata'; +export { default as useStorage } from './useStorage'; diff --git a/web-marketplace/src/hooks/useLocalStorage.ts b/web-marketplace/src/hooks/useStorage.ts similarity index 68% rename from web-marketplace/src/hooks/useLocalStorage.ts rename to web-marketplace/src/hooks/useStorage.ts index b86b11962b..6703734a6e 100644 --- a/web-marketplace/src/hooks/useLocalStorage.ts +++ b/web-marketplace/src/hooks/useStorage.ts @@ -7,22 +7,25 @@ interface StorageApi { removeData: () => void; } -export default function useLocalStorage( +export default function useStorage( key: string, + useLocalStorage: boolean, initialValue?: T, ): StorageApi { const [data, saveData] = useState(() => { // this way, as a fn, this initialization happens just once - try { - const value = localStorage.getItem(key); - return value ? JSON.parse(value) : initialValue; - } catch (err) { - return initialValue; - } + if (useLocalStorage) { + try { + const value = localStorage.getItem(key); + return value ? JSON.parse(value) : initialValue; + } catch (err) { + return initialValue; + } + } else return undefined; }); useEffect(() => { - if (data) { + if (useLocalStorage && data) { // TODO: if (_.isEqual(data, initialValues)) return; try { localStorage.setItem(key, JSON.stringify(data)); @@ -31,12 +34,12 @@ export default function useLocalStorage( console.log(err); } } - }, [data, key]); + }, [data, key, useLocalStorage]); // TODO: Remove data (key/value) const removeData = (): void => { try { - localStorage.removeItem(key); + if (useLocalStorage) localStorage.removeItem(key); // reset state to initial values saveData(initialValue); } catch (err) { diff --git a/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx b/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx index 02e562250b..a06fe5e694 100644 --- a/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx +++ b/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx @@ -103,6 +103,7 @@ export const BuyCreditsForm = ({ throw new Error('Function not implemented.'); }} retiring={retiring} + stripePublishableKey={import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY} /> )} {activeStep === 2 && ( diff --git a/web-marketplace/src/pages/BuyCredits/BuyCredits.constants.ts b/web-marketplace/src/pages/BuyCredits/BuyCredits.constants.ts index e412d52dd4..a89c58cfbf 100644 --- a/web-marketplace/src/pages/BuyCredits/BuyCredits.constants.ts +++ b/web-marketplace/src/pages/BuyCredits/BuyCredits.constants.ts @@ -1,4 +1,7 @@ +import { msg } from '@lingui/macro'; + export const PAYMENT_OPTIONS = { CARD: 'card', CRYPTO: 'crypto', } as const; +export const NEXT = msg`next`; diff --git a/web-marketplace/src/pages/BuyCredits/BuyCredits.tsx b/web-marketplace/src/pages/BuyCredits/BuyCredits.tsx index 7845cf608b..74c270d172 100644 --- a/web-marketplace/src/pages/BuyCredits/BuyCredits.tsx +++ b/web-marketplace/src/pages/BuyCredits/BuyCredits.tsx @@ -66,6 +66,7 @@ export const BuyCredits = () => { formId={formModel.formId} steps={formModel.steps} initialValues={{}} + useLocalStorage={false} > Date: Thu, 19 Sep 2024 12:09:43 +0200 Subject: [PATCH 02/10] feat: add more props to PaymentInfoForm, payment methods query and some refactoring --- .../molecules/CreditsAmount/CreditsAmount.tsx | 119 ++++++------------ .../CreditsAmount/CreditsAmount.utils.tsx | 117 +++++++++++++++++ .../PaymentInfoForm.CustomerInfo.tsx | 2 +- .../PaymentInfoForm.PaymentInfo.tsx | 2 +- .../PaymentInfoForm/PaymentInfoForm.schema.ts | 2 +- .../PaymentInfoForm/PaymentInfoForm.tsx | 19 ++- .../getPaymentMethodsQuery.constants.ts | 1 + .../getPaymentMethodsQuery.ts | 31 +++++ .../getPaymentMethodsQuery.types.ts | 12 ++ .../src/pages/BuyCredits/BuyCredits.Form.tsx | 55 ++++---- 10 files changed, 239 insertions(+), 121 deletions(-) create mode 100644 web-marketplace/src/lib/queries/react-query/registry-server/getPaymentMethodsQuery/getPaymentMethodsQuery.constants.ts create mode 100644 web-marketplace/src/lib/queries/react-query/registry-server/getPaymentMethodsQuery/getPaymentMethodsQuery.ts create mode 100644 web-marketplace/src/lib/queries/react-query/registry-server/getPaymentMethodsQuery/getPaymentMethodsQuery.types.ts diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx index 0e38bfa5ab..89ab8aec3c 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx @@ -17,7 +17,11 @@ import { import { CreditsAmountHeader } from './CreditsAmount.Header'; import { CreditsAmountProps } from './CreditsAmount.types'; import { + formatFullSellOrder, + getCreditsAmount, getCreditsAvailablePerCurrency, + getCurrencyAmount, + getSellOrderPrice, getSpendingCap, } from './CreditsAmount.utils'; import { CreditsInput } from './CreditsInput'; @@ -84,10 +88,30 @@ export const CreditsAmount = ({ useEffect(() => { if (maxCreditsSelected) { setValue(CREDITS_AMOUNT, creditsAvailable); - setValue(CURRENCY_AMOUNT, microToDenom(spendingCap)); + setValue( + CURRENCY_AMOUNT, + paymentOption === PAYMENT_OPTIONS.CARD + ? spendingCap + : microToDenom(spendingCap), + ); + setValue( + SELL_ORDERS, + orderedSellOrders.map(order => { + const price = getSellOrderPrice({ order, card }); + return formatFullSellOrder({ order, card, price }); + }), + ); setMaxCreditsSelected(false); } - }, [creditsAvailable, maxCreditsSelected, setValue, spendingCap]); + }, [ + card, + creditsAvailable, + maxCreditsSelected, + orderedSellOrders, + paymentOption, + setValue, + spendingCap, + ]); // Credits amount change const handleCreditsAmountChange = useCallback( @@ -95,48 +119,12 @@ export const CreditsAmount = ({ // Set currency amount according to credits quantity, // selecting the cheapest credits first const currentCreditsAmount = e.target.valueAsNumber; - let currentCurrencyAmount = 0; - let creditsAmountLeft = currentCreditsAmount; - const sellOrders = []; - - for (let i = 0; i < orderedSellOrders.length; i++) { - const order = orderedSellOrders[i]; - const price = card - ? (order as CardSellOrder).usdPrice - : Number(order.askAmount); - const quantity = Number(order.quantity); - - // Take all credits from this sell order - if (creditsAmountLeft >= quantity) { - creditsAmountLeft -= quantity; - currentCurrencyAmount += quantity * price; - sellOrders.push({ - sellOrderId: order.id, - quantity: order.quantity, - bidPrice: !card - ? { amount: String(price), denom: order.askDenom } - : undefined, - price: card ? price : undefined, - }); - if (creditsAmountLeft === 0) break; - } else { - // Take only remaining credits - currentCurrencyAmount += creditsAmountLeft * price; - sellOrders.push({ - sellOrderId: order.id, - quantity: String(creditsAmountLeft), - bidPrice: !card - ? { amount: String(price), denom: order.askDenom } - : undefined, - price: card ? price : undefined, - }); - break; - } - } - setValue( - CURRENCY_AMOUNT, - card ? currentCurrencyAmount : microToDenom(currentCurrencyAmount), - ); + const { currencyAmount, sellOrders } = getCurrencyAmount({ + currentCreditsAmount, + card, + orderedSellOrders, + }); + setValue(CURRENCY_AMOUNT, currencyAmount); setValue(SELL_ORDERS, sellOrders); }, [card, orderedSellOrders, setValue], @@ -148,44 +136,11 @@ export const CreditsAmount = ({ // Set credits quantity according to currency amount, // selecting the cheapest credits first const value = e.target.valueAsNumber; - const currentCurrencyAmount = card ? value : denomToMicro(value); - let currentCreditsAmount = 0; - let currencyAmountLeft = currentCurrencyAmount; - const sellOrders = []; - - for (let i = 0; i < orderedSellOrders.length; i++) { - const order = orderedSellOrders[i]; - const price = card - ? (order as CardSellOrder).usdPrice - : Number(order.askAmount); - const quantity = Number(order.quantity); - const orderTotalAmount = quantity * price; - - if (currencyAmountLeft >= orderTotalAmount) { - currencyAmountLeft -= orderTotalAmount; - currentCreditsAmount += quantity; - sellOrders.push({ - sellOrderId: order.id, - quantity: order.quantity, - bidPrice: !card - ? { amount: String(price), denom: order.askDenom } - : undefined, - price: card ? price : undefined, - }); - if (currencyAmountLeft === 0) break; - } else { - currentCreditsAmount += currencyAmountLeft / price; - sellOrders.push({ - sellOrderId: order.id, - quantity: String(currencyAmountLeft / price), - bidPrice: !card - ? { amount: String(price), denom: order.askDenom } - : undefined, - price: card ? price : undefined, - }); - break; - } - } + const { currentCreditsAmount, sellOrders } = getCreditsAmount({ + value, + card, + orderedSellOrders, + }); setValue(CREDITS_AMOUNT, currentCreditsAmount); setValue(SELL_ORDERS, sellOrders); }, diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx index 137176dd2c..6300905cc0 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx @@ -1,9 +1,13 @@ import { CardSellOrder } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.types'; +import { denomToMicro, microToDenom } from 'lib/denom.utils'; + import { PAYMENT_OPTIONS } from 'pages/BuyCredits/BuyCredits.constants'; import { PaymentOptionsType } from 'pages/BuyCredits/BuyCredits.types'; import { UISellOrderInfo } from 'pages/Projects/AllProjects/AllProjects.types'; +import { CURRENCY_AMOUNT } from './CreditsAmount.constants'; + export const getCreditsAvailablePerCurrency = ( paymentOption: PaymentOptionsType, filteredCryptoSellOrders: Array | undefined, @@ -34,3 +38,116 @@ export function getSpendingCap( 0, ) || 0; } + +type GetCreditsAmountParams = { + value: number; + card: boolean; + orderedSellOrders: UISellOrderInfo[]; +}; +export const getCreditsAmount = ({ + value, + card, + orderedSellOrders, +}: GetCreditsAmountParams) => { + const currentCurrencyAmount = card ? value : denomToMicro(value); + let currentCreditsAmount = 0; + let currencyAmountLeft = currentCurrencyAmount; + const sellOrders = []; + + for (let i = 0; i < orderedSellOrders.length; i++) { + const order = orderedSellOrders[i]; + const price = getSellOrderPrice({ order, card }); + const quantity = Number(order.quantity); + const orderTotalAmount = quantity * price; + + if (currencyAmountLeft >= orderTotalAmount) { + currencyAmountLeft -= orderTotalAmount; + currentCreditsAmount += quantity; + sellOrders.push(formatFullSellOrder({ order, card, price })); + if (currencyAmountLeft === 0) break; + } else { + currentCreditsAmount += currencyAmountLeft / price; + sellOrders.push({ + sellOrderId: order.id, + quantity: String(currencyAmountLeft / price), + bidPrice: !card + ? { amount: String(price), denom: order.askDenom } + : undefined, + price: card ? price : undefined, + }); + break; + } + } + return { currentCreditsAmount, sellOrders }; +}; + +type GetCurrencyAmountParams = { + currentCreditsAmount: number; + card: boolean; + orderedSellOrders: UISellOrderInfo[]; +}; +export const getCurrencyAmount = ({ + currentCreditsAmount, + card, + orderedSellOrders, +}: GetCurrencyAmountParams) => { + let currentCurrencyAmount = 0; + let creditsAmountLeft = currentCreditsAmount; + const sellOrders = []; + + for (let i = 0; i < orderedSellOrders.length; i++) { + const order = orderedSellOrders[i]; + const price = getSellOrderPrice({ order, card }); + const quantity = Number(order.quantity); + + // Take all credits from this sell order + if (creditsAmountLeft >= quantity) { + creditsAmountLeft -= quantity; + currentCurrencyAmount += quantity * price; + sellOrders.push(formatFullSellOrder({ order, card, price })); + + if (creditsAmountLeft === 0) break; + } else { + // Take only remaining credits + currentCurrencyAmount += creditsAmountLeft * price; + sellOrders.push({ + sellOrderId: order.id, + quantity: String(creditsAmountLeft), + bidPrice: !card + ? { amount: String(price), denom: order.askDenom } + : undefined, + price: card ? price : undefined, + }); + break; + } + } + return { + [CURRENCY_AMOUNT]: card + ? currentCurrencyAmount + : microToDenom(currentCurrencyAmount), + sellOrders, + }; +}; + +type GetSellOrderPriceParams = { + order: UISellOrderInfo; + card: boolean; +}; +export const getSellOrderPrice = ({ order, card }: GetSellOrderPriceParams) => + card ? (order as CardSellOrder).usdPrice : Number(order.askAmount); + +type FormatFullSellOrderParams = { price: number } & GetSellOrderPriceParams; +export const formatFullSellOrder = ({ + order, + card, + price, +}: FormatFullSellOrderParams) => { + return { + sellOrderId: order.id, + quantity: order.quantity, + bidPrice: !card + ? { amount: String(price), denom: order.askDenom } + : undefined, + price: card ? price : undefined, + }; +}; diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx index d719749c7f..087dc4a2a5 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx @@ -19,7 +19,7 @@ export type CustomerInfoProps = { paymentOption: PaymentOptionsType; wallet?: Wallet; accountId?: string; - accountEmail?: string; + accountEmail?: string | null; accountName?: string; login: () => void; retiring: boolean; diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.PaymentInfo.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.PaymentInfo.tsx index 5e505fa242..fdafc3ba20 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.PaymentInfo.tsx +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.PaymentInfo.tsx @@ -12,7 +12,7 @@ import { CardInfo } from './PaymentInfoForm.CardInfo'; import { PaymentInfoFormSchemaType } from './PaymentInfoForm.schema'; export type PaymentInfoProps = { - paymentMethods?: Array; + paymentMethods?: Array | null; accountId?: string; }; export const PaymentInfo = ({ diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.schema.ts b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.schema.ts index a072ac68f3..e388c52569 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.schema.ts +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.schema.ts @@ -10,7 +10,7 @@ export const paymentInfoFormSchema = (paymentOption: PaymentOptionsType) => email: paymentOption === PAYMENT_OPTIONS.CARD ? z.string().email().min(1) - : z.union([z.literal(''), z.string().email()]), + : z.union([z.literal(''), z.string().email().nullable()]), createAccount: z.boolean(), savePaymentMethod: z.boolean(), paymentMethodId: z.string().optional(), diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx index b10a5e04bd..62152399fa 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx @@ -60,12 +60,9 @@ export const PaymentInfoForm = ({ }, mode: 'onBlur', }); - const { isValid, isSubmitting, errors } = useFormState({ + const { isValid, isSubmitting } = useFormState({ control: form.control, }); - console.log('values', form.getValues()); - console.log('isValid', isValid); - console.log('errors', errors); const stripePromise = useMemo( () => @@ -132,13 +129,13 @@ export const PaymentInfoForm = ({ /> )} -
- -
+ +
+
); diff --git a/web-marketplace/src/lib/queries/react-query/registry-server/getPaymentMethodsQuery/getPaymentMethodsQuery.constants.ts b/web-marketplace/src/lib/queries/react-query/registry-server/getPaymentMethodsQuery/getPaymentMethodsQuery.constants.ts new file mode 100644 index 0000000000..d31145ea0e --- /dev/null +++ b/web-marketplace/src/lib/queries/react-query/registry-server/getPaymentMethodsQuery/getPaymentMethodsQuery.constants.ts @@ -0,0 +1 @@ +export const GET_PAYMENT_METHODS_QUERY_KEY = 'getPost'; diff --git a/web-marketplace/src/lib/queries/react-query/registry-server/getPaymentMethodsQuery/getPaymentMethodsQuery.ts b/web-marketplace/src/lib/queries/react-query/registry-server/getPaymentMethodsQuery/getPaymentMethodsQuery.ts new file mode 100644 index 0000000000..449c712bb7 --- /dev/null +++ b/web-marketplace/src/lib/queries/react-query/registry-server/getPaymentMethodsQuery/getPaymentMethodsQuery.ts @@ -0,0 +1,31 @@ +import { apiUri } from 'lib/apiUri'; + +import { GET_PAYMENT_METHODS_QUERY_KEY } from './getPaymentMethodsQuery.constants'; +import { + ReactQueryGetPostQueryParams, + ReactQueryGetPostQueryResponse, +} from './getPaymentMethodsQuery.types'; + +export const getPaymentMethodsQuery = ({ + limit, + ...params +}: ReactQueryGetPostQueryParams): ReactQueryGetPostQueryResponse => ({ + queryKey: [GET_PAYMENT_METHODS_QUERY_KEY, limit], + queryFn: async () => { + try { + const resp = await fetch( + `${apiUri}/marketplace/v1/stripe/payment-methods${ + limit ? `?limit=${limit}` : '' + }`, + { + method: 'GET', + credentials: 'include', + }, + ); + return await resp.json(); + } catch (e) { + return null; + } + }, + ...params, +}); diff --git a/web-marketplace/src/lib/queries/react-query/registry-server/getPaymentMethodsQuery/getPaymentMethodsQuery.types.ts b/web-marketplace/src/lib/queries/react-query/registry-server/getPaymentMethodsQuery/getPaymentMethodsQuery.types.ts new file mode 100644 index 0000000000..99accc69e5 --- /dev/null +++ b/web-marketplace/src/lib/queries/react-query/registry-server/getPaymentMethodsQuery/getPaymentMethodsQuery.types.ts @@ -0,0 +1,12 @@ +import { PaymentMethod } from '@stripe/stripe-js'; +import { QueryObserverOptions } from '@tanstack/react-query'; + +import { ReactQueryBuilderResponse } from '../../types/react-query.types'; + +export type ReactQueryGetPostQueryResponse = QueryObserverOptions<{ + paymentMethods?: PaymentMethod[] | null; +}>; + +export type ReactQueryGetPostQueryParams = { + limit?: number; +} & ReactQueryBuilderResponse; diff --git a/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx b/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx index a06fe5e694..501737d166 100644 --- a/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx +++ b/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx @@ -3,6 +3,7 @@ import { msg, Trans } from '@lingui/macro'; import { useLingui } from '@lingui/react'; import { SellOrderInfo } from '@regen-network/api/lib/generated/regen/ecocredit/marketplace/v1/query'; import { useQuery } from '@tanstack/react-query'; +import { USD_DENOM } from 'config/allowedBaseDenoms'; import ContainedButton from 'web-components/src/components/buttons/ContainedButton'; import SaveFooter from 'web-components/src/components/fixed-footer/SaveFooter'; @@ -10,15 +11,21 @@ import { PrevNextButtons } from 'web-components/src/components/molecules/PrevNex import { UseStateSetter } from 'web-components/src/types/react/useState'; import { useLedger } from 'ledger'; +import { useAuth } from 'lib/auth/auth'; import { getCreditTypeQuery } from 'lib/queries/react-query/ecocredit/getCreditTypeQuery/getCreditTypeQuery'; import { getAllowedDenomQuery } from 'lib/queries/react-query/ecocredit/marketplace/getAllowedDenomQuery/getAllowedDenomQuery'; +import { getPaymentMethodsQuery } from 'lib/queries/react-query/registry-server/getPaymentMethodsQuery/getPaymentMethodsQuery'; +import { useWallet } from 'lib/wallet/wallet'; import { UISellOrderInfo } from 'pages/Projects/AllProjects/AllProjects.types'; +import { CURRENCY_AMOUNT } from 'components/molecules/CreditsAmount/CreditsAmount.constants'; import { AgreePurchaseForm } from 'components/organisms/AgreePurchaseForm/AgreePurchaseForm'; +import { AgreePurchaseFormSchemaType } from 'components/organisms/AgreePurchaseForm/AgreePurchaseForm.schema'; import { ChooseCreditsForm } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm'; import { ChooseCreditsFormSchemaType } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema'; import { CardSellOrder } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm.types'; import { PaymentInfoForm } from 'components/organisms/PaymentInfoForm/PaymentInfoForm'; +import { PaymentInfoFormSchemaType } from 'components/organisms/PaymentInfoForm/PaymentInfoForm.schema'; import { useMultiStep } from 'components/templates/MultiStepTemplate'; import { PaymentOptionsType } from './BuyCredits.types'; @@ -43,11 +50,17 @@ export const BuyCreditsForm = ({ creditTypeAbbrev, projectHref, }: Props) => { - const { data, activeStep, handleSaveNext } = useMultiStep(); - console.log('data', data); + const { data, activeStep, handleSaveNext } = useMultiStep< + Partial & + Partial & + Partial + >(); + const { wallet } = useWallet(); + const { activeAccount, privActiveAccount } = useAuth(); const cardDisabled = cardSellOrders.length === 0; const { marketplaceClient, ecocreditClient } = useLedger(); + const { data: allowedDenomsData } = useQuery( getAllowedDenomQuery({ client: marketplaceClient, @@ -65,6 +78,12 @@ export const BuyCreditsForm = ({ }), ); + const { data: paymentMethodData } = useQuery( + getPaymentMethodsQuery({ + enabled: !!activeAccount, + }), + ); + return (
@@ -88,39 +107,25 @@ export const BuyCreditsForm = ({ {activeStep === 1 && ( { - throw new Error('Function not implemented.'); - }} - amount={0} - currency={''} + onSubmit={async (values: PaymentInfoFormSchemaType) => {}} + amount={(data?.[CURRENCY_AMOUNT] ?? 0) * 100} // stripe amounts should be in the smallest currency unit (e.g., 100 cents to charge $1.00) + currency={USD_DENOM} login={function (): void { throw new Error('Function not implemented.'); }} retiring={retiring} stripePublishableKey={import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY} + wallet={wallet} + accountEmail={privActiveAccount?.email} + accountName={activeAccount?.name} + accountId={activeAccount?.id} + paymentMethods={paymentMethodData?.paymentMethods} /> )} {activeStep === 2 && ( { - throw new Error('Function not implemented.'); - }} + onSubmit={async (values: AgreePurchaseFormSchemaType) => {}} goToChooseCredits={function (): void { throw new Error('Function not implemented.'); }} From 388ff60bd962fcea4accac33dca1f57f6941e48f Mon Sep 17 00:00:00 2001 From: blushi Date: Thu, 19 Sep 2024 13:03:37 +0200 Subject: [PATCH 03/10] feat: add login to PaymentInfoForm --- .../PaymentInfoForm.CustomerInfo.tsx | 2 +- .../PaymentInfoForm/PaymentInfoForm.tsx | 10 +++++++++- .../getSellOrdersExtendedQuery.ts | 1 - .../src/pages/BuyCredits/BuyCredits.Form.tsx | 20 ++++++++++++++++--- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx index 087dc4a2a5..e2b0426060 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx @@ -50,7 +50,7 @@ export const CustomerInfo = ({ <Trans>Customer info</Trans> - {!accountId && !wallet && ( + {!accountId && !wallet?.address && ( log in for faster checkout diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx index 62152399fa..ce18202eab 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { useFormState } from 'react-hook-form'; import { useLingui } from '@lingui/react'; import { Elements } from '@stripe/react-stripe-js'; @@ -109,6 +109,14 @@ export const PaymentInfoForm = ({ [amount, currency], ); + useEffect(() => { + // set form values after login + if (accountEmail) form.setValue('email', accountEmail); + if (accountName) form.setValue('name', accountName); + if (paymentMethods) + form.setValue('paymentMethodId', paymentMethods?.[0]?.id); + }, [accountEmail, accountName, form, paymentMethods]); + return (
diff --git a/web-marketplace/src/lib/queries/react-query/ecocredit/marketplace/getSellOrdersExtendedQuery/getSellOrdersExtendedQuery.ts b/web-marketplace/src/lib/queries/react-query/ecocredit/marketplace/getSellOrdersExtendedQuery/getSellOrdersExtendedQuery.ts index 77497a7f65..7fc50cfd1f 100644 --- a/web-marketplace/src/lib/queries/react-query/ecocredit/marketplace/getSellOrdersExtendedQuery/getSellOrdersExtendedQuery.ts +++ b/web-marketplace/src/lib/queries/react-query/ecocredit/marketplace/getSellOrdersExtendedQuery/getSellOrdersExtendedQuery.ts @@ -41,7 +41,6 @@ export const getSellOrdersExtendedQuery = ({ response = await client.SellOrders(request); sellOrders.push(...response.sellOrders); } - console.log(sellOrders); // Find sell orders that have ibc askDenom and gather their hash const ibcDenomHashes = uniq( diff --git a/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx b/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx index 501737d166..599218c247 100644 --- a/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx +++ b/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx @@ -24,6 +24,8 @@ import { AgreePurchaseFormSchemaType } from 'components/organisms/AgreePurchaseF import { ChooseCreditsForm } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm'; import { ChooseCreditsFormSchemaType } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema'; import { CardSellOrder } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm.types'; +import { useLoginData } from 'components/organisms/LoginButton/hooks/useLoginData'; +import { LoginFlow } from 'components/organisms/LoginFlow/LoginFlow'; import { PaymentInfoForm } from 'components/organisms/PaymentInfoForm/PaymentInfoForm'; import { PaymentInfoFormSchemaType } from 'components/organisms/PaymentInfoForm/PaymentInfoForm.schema'; import { useMultiStep } from 'components/templates/MultiStepTemplate'; @@ -57,6 +59,14 @@ export const BuyCreditsForm = ({ >(); const { wallet } = useWallet(); const { activeAccount, privActiveAccount } = useAuth(); + const { + isModalOpen, + modalState, + onModalClose, + walletsUiConfig, + onButtonClick, + } = useLoginData({}); + const cardDisabled = cardSellOrders.length === 0; const { marketplaceClient, ecocreditClient } = useLedger(); @@ -110,9 +120,7 @@ export const BuyCreditsForm = ({ onSubmit={async (values: PaymentInfoFormSchemaType) => {}} amount={(data?.[CURRENCY_AMOUNT] ?? 0) * 100} // stripe amounts should be in the smallest currency unit (e.g., 100 cents to charge $1.00) currency={USD_DENOM} - login={function (): void { - throw new Error('Function not implemented.'); - }} + login={onButtonClick} retiring={retiring} stripePublishableKey={import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY} wallet={wallet} @@ -133,6 +141,12 @@ export const BuyCreditsForm = ({ /> )}
+
); }; From 192211f79d73f87f3c7bf233573c149ef2a995f6 Mon Sep 17 00:00:00 2001 From: blushi Date: Thu, 19 Sep 2024 17:17:26 +0200 Subject: [PATCH 04/10] refactor: mv stripe Elements up and get confirmation token --- .../molecules/CreditsAmount/CreditsAmount.tsx | 2 +- .../CreditsAmount/CreditsAmount.utils.tsx | 1 + .../PaymentInfoForm.constants.ts | 36 +++++- .../PaymentInfoForm.stories.tsx | 54 ++++---- .../PaymentInfoForm/PaymentInfoForm.tsx | 119 ++++++++---------- .../src/pages/BuyCredits/BuyCredits.Form.tsx | 61 +++++---- .../pages/BuyCredits/BuyCredits.constants.ts | 2 + 7 files changed, 158 insertions(+), 117 deletions(-) diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx index 89ab8aec3c..8bf3df214b 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx @@ -56,7 +56,7 @@ export const CreditsAmount = ({ [card, cardSellOrders, filteredCryptoSellOrders], ); - + console.log('orderedSellOrders', orderedSellOrders); useEffect(() => { // Reset amounts to 0 on currency change setValue(CREDITS_AMOUNT, 0); diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx index 6300905cc0..031e87a33e 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx @@ -121,6 +121,7 @@ export const getCurrencyAmount = ({ break; } } + console.log('currentCurrencyAmount', microToDenom(currentCurrencyAmount)); return { [CURRENCY_AMOUNT]: card ? currentCurrencyAmount diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.constants.ts b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.constants.ts index 242d3dc252..fecad4357b 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.constants.ts +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.constants.ts @@ -1,5 +1,39 @@ -import { Layout } from '@stripe/stripe-js'; +import { Layout, StripeElementsOptions } from '@stripe/stripe-js'; + +import { defaultFontFamily } from 'web-components/src/theme/muiTheme'; export const paymentElementOptions = { layout: 'tabs' as Layout, }; + +export const defaultStripeOptions: StripeElementsOptions = { + mode: 'payment', + paymentMethodCreation: 'manual', + fonts: [ + { + cssSrc: + 'https://fonts.googleapis.com/css?family=Lato:100,300,400,700,800', + }, + ], + appearance: { + theme: 'stripe', + variables: { + colorText: '#000', + colorDanger: '#DE4526', + fontFamily: defaultFontFamily, + spacingUnit: '5px', + borderRadius: '2px', + }, + rules: { + '.Label': { + fontWeight: 'bold', + fontSize: '16px', + }, + '.Input': { + boxShadow: 'none', + borderColor: '#D2D5D9', + marginTop: '9px', + }, + }, + }, +}; diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.stories.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.stories.tsx index c42ec81247..62a2a6e1b0 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.stories.tsx +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.stories.tsx @@ -1,7 +1,12 @@ import { action } from '@storybook/addon-actions'; import { Meta, StoryObj } from '@storybook/react'; +import { Elements } from '@stripe/react-stripe-js'; +import { loadStripe } from '@stripe/stripe-js'; -import { PaymentInfoForm } from './PaymentInfoForm'; +import { PAYMENT_OPTIONS } from 'pages/BuyCredits/BuyCredits.constants'; + +import { PaymentInfoForm, PaymentInfoFormProps } from './PaymentInfoForm'; +import { defaultStripeOptions } from './PaymentInfoForm.constants'; export default { title: 'Marketplace/Organisms/PaymentInfoForm', @@ -10,21 +15,33 @@ export default { type Story = StoryObj; +const stripeKey = import.meta.env.STORYBOOK_STRIPE_PUBLISHABLE_KEY; + +const WrappedPaymentInfoForm = (args: PaymentInfoFormProps) => { + const options = { amount: 1000, currency: 'usd', ...defaultStripeOptions }; + const stripePromise = + args.paymentOption === PAYMENT_OPTIONS.CARD && + stripeKey && + loadStripe(stripeKey); + + return ( + + + + ); +}; export const FiatLoggedOut: Story = { - render: args => , + render: args => , }; FiatLoggedOut.args = { paymentOption: 'card', login: action('login'), - stripePublishableKey: import.meta.env.STORYBOOK_STRIPE_PUBLISHABLE_KEY, - amount: 1000, - currency: 'usd', retiring: true, }; export const FiatLoggedInNoEmail: Story = { - render: args => , + render: args => , }; FiatLoggedInNoEmail.args = { @@ -33,14 +50,11 @@ FiatLoggedInNoEmail.args = { accountName: 'John Doe', wallet: { address: 'regen123456', shortAddress: 'regen123' }, login: action('login'), - stripePublishableKey: import.meta.env.STORYBOOK_STRIPE_PUBLISHABLE_KEY, - amount: 1000, - currency: 'usd', retiring: true, }; export const FiatLoggedInWithEmail: Story = { - render: args => , + render: args => , }; FiatLoggedInWithEmail.args = { @@ -49,14 +63,11 @@ FiatLoggedInWithEmail.args = { accountEmail: 'john@doe.com', accountName: 'John Doe', login: action('login'), - stripePublishableKey: import.meta.env.STORYBOOK_STRIPE_PUBLISHABLE_KEY, - amount: 1000, - currency: 'usd', retiring: true, }; export const FiatLoggedInWithPaymentMethod: Story = { - render: args => , + render: args => , }; FiatLoggedInWithPaymentMethod.args = { @@ -65,9 +76,6 @@ FiatLoggedInWithPaymentMethod.args = { accountEmail: 'john@doe.com', accountName: 'John Doe', login: action('login'), - stripePublishableKey: import.meta.env.STORYBOOK_STRIPE_PUBLISHABLE_KEY, - amount: 1000, - currency: 'usd', retiring: true, paymentMethods: [ { @@ -112,33 +120,29 @@ FiatLoggedInWithPaymentMethod.args = { }; export const CryptoNoEmail: Story = { - render: args => , + render: args => , }; CryptoNoEmail.args = { paymentOption: 'crypto', wallet: { address: 'regen123456', shortAddress: 'regen123' }, login: action('login'), - amount: 1000, - currency: 'usd', retiring: true, }; export const CryptoTradableCredits: Story = { - render: args => , + render: args => , }; CryptoTradableCredits.args = { paymentOption: 'crypto', wallet: { address: 'regen123456', shortAddress: 'regen123' }, login: action('login'), - amount: 1000, - currency: 'usd', retiring: false, }; export const CryptoWithEmail: Story = { - render: args => , + render: args => , }; CryptoWithEmail.args = { @@ -147,7 +151,5 @@ CryptoWithEmail.args = { accountEmail: 'john@doe.com', accountName: 'John Doe', login: action('login'), - amount: 1000, - currency: 'usd', retiring: true, }; diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx index ce18202eab..6939e0b484 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx @@ -1,11 +1,9 @@ -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; import { useFormState } from 'react-hook-form'; import { useLingui } from '@lingui/react'; -import { Elements } from '@stripe/react-stripe-js'; -import { loadStripe, StripeElementsOptionsMode } from '@stripe/stripe-js'; +import { useElements, useStripe } from '@stripe/react-stripe-js'; import { PrevNextButtons } from 'web-components/src/components/molecules/PrevNextButtons/PrevNextButtons'; -import { defaultFontFamily } from 'web-components/src/theme/muiTheme'; import { NEXT, PAYMENT_OPTIONS } from 'pages/BuyCredits/BuyCredits.constants'; import { PaymentOptionsType } from 'pages/BuyCredits/BuyCredits.types'; @@ -23,12 +21,12 @@ import { PaymentInfoFormSchemaType, } from './PaymentInfoForm.schema'; -type PaymentInfoFormProps = { +export type PaymentInfoFormProps = { paymentOption: PaymentOptionsType; - onSubmit: (values: PaymentInfoFormSchemaType) => Promise; - stripePublishableKey?: string; - amount: number; - currency: string; + onSubmit: ( + values: PaymentInfoFormSchemaType & { confirmationTokenId?: string }, + ) => Promise; + setError: (error: string) => void; } & CustomerInfoProps & PaymentInfoProps; @@ -41,13 +39,13 @@ export const PaymentInfoForm = ({ onSubmit, login, paymentMethods, - stripePublishableKey, - amount, - currency, + setError, retiring, }: PaymentInfoFormProps) => { const { _ } = useLingui(); const { handleBack } = useMultiStep(); + const stripe = useStripe(); + const elements = useElements(); const form = useZodForm({ schema: paymentInfoFormSchema(paymentOption), @@ -64,51 +62,6 @@ export const PaymentInfoForm = ({ control: form.control, }); - const stripePromise = useMemo( - () => - paymentOption === PAYMENT_OPTIONS.CARD && - stripePublishableKey && - loadStripe(stripePublishableKey), - [paymentOption, stripePublishableKey], - ); - - const options: StripeElementsOptionsMode = useMemo( - () => ({ - mode: 'payment', - amount, - currency, - paymentMethodCreation: 'manual', - fonts: [ - { - cssSrc: - 'https://fonts.googleapis.com/css?family=Lato:100,300,400,700,800', - }, - ], - appearance: { - theme: 'stripe', - variables: { - colorText: '#000', - colorDanger: '#DE4526', - fontFamily: defaultFontFamily, - spacingUnit: '5px', - borderRadius: '2px', - }, - rules: { - '.Label': { - fontWeight: 'bold', - fontSize: '16px', - }, - '.Input': { - boxShadow: 'none', - borderColor: '#D2D5D9', - marginTop: '9px', - }, - }, - }, - }), - [amount, currency], - ); - useEffect(() => { // set form values after login if (accountEmail) form.setValue('email', accountEmail); @@ -118,7 +71,42 @@ export const PaymentInfoForm = ({ }, [accountEmail, accountName, form, paymentMethods]); return ( - + { + const card = paymentOption === PAYMENT_OPTIONS.CARD; + if (card && !stripe) { + return; + } + let confirmationTokenId: string | undefined; + if (card && stripe && elements && !values.paymentMethodId) { + const submitRes = await elements?.submit(); + if (submitRes?.error?.message) { + setError(submitRes?.error?.message); + return; + } + // Create the ConfirmationToken using the details collected by the Payment Element + const { error, confirmationToken } = + await stripe.createConfirmationToken({ + elements, + params: { + payment_method_data: { + billing_details: { + name: values.name, + email: values.email, + }, + }, + }, + }); + if (error?.message) { + setError(error?.message); + return; + } + confirmationTokenId = confirmationToken?.id; + } + onSubmit({ confirmationTokenId, ...values }); + }} + >
- {paymentOption === PAYMENT_OPTIONS.CARD && stripePromise && ( - - - + {paymentOption === PAYMENT_OPTIONS.CARD && ( + )}
diff --git a/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx b/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx index 599218c247..7e1e8fa246 100644 --- a/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx +++ b/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx @@ -1,16 +1,14 @@ -import { useNavigate } from 'react-router-dom'; -import { msg, Trans } from '@lingui/macro'; -import { useLingui } from '@lingui/react'; -import { SellOrderInfo } from '@regen-network/api/lib/generated/regen/ecocredit/marketplace/v1/query'; +import { useMemo } from 'react'; +import { Elements, useElements, useStripe } from '@stripe/react-stripe-js'; +import { loadStripe } from '@stripe/stripe-js'; import { useQuery } from '@tanstack/react-query'; import { USD_DENOM } from 'config/allowedBaseDenoms'; +import { useSetAtom } from 'jotai'; -import ContainedButton from 'web-components/src/components/buttons/ContainedButton'; -import SaveFooter from 'web-components/src/components/fixed-footer/SaveFooter'; -import { PrevNextButtons } from 'web-components/src/components/molecules/PrevNextButtons/PrevNextButtons'; import { UseStateSetter } from 'web-components/src/types/react/useState'; import { useLedger } from 'ledger'; +import { errorBannerTextAtom } from 'lib/atoms/error.atoms'; import { useAuth } from 'lib/auth/auth'; import { getCreditTypeQuery } from 'lib/queries/react-query/ecocredit/getCreditTypeQuery/getCreditTypeQuery'; import { getAllowedDenomQuery } from 'lib/queries/react-query/ecocredit/marketplace/getAllowedDenomQuery/getAllowedDenomQuery'; @@ -27,9 +25,11 @@ import { CardSellOrder } from 'components/organisms/ChooseCreditsForm/ChooseCred import { useLoginData } from 'components/organisms/LoginButton/hooks/useLoginData'; import { LoginFlow } from 'components/organisms/LoginFlow/LoginFlow'; import { PaymentInfoForm } from 'components/organisms/PaymentInfoForm/PaymentInfoForm'; +import { defaultStripeOptions } from 'components/organisms/PaymentInfoForm/PaymentInfoForm.constants'; import { PaymentInfoFormSchemaType } from 'components/organisms/PaymentInfoForm/PaymentInfoForm.schema'; import { useMultiStep } from 'components/templates/MultiStepTemplate'; +import { PAYMENT_OPTIONS, stripeKey } from './BuyCredits.constants'; import { PaymentOptionsType } from './BuyCredits.types'; type Props = { @@ -54,8 +54,9 @@ export const BuyCreditsForm = ({ }: Props) => { const { data, activeStep, handleSaveNext } = useMultiStep< Partial & - Partial & - Partial + Partial & { + confirmationTokenId?: string; + } & Partial >(); const { wallet } = useWallet(); const { activeAccount, privActiveAccount } = useAuth(); @@ -67,6 +68,8 @@ export const BuyCreditsForm = ({ onButtonClick, } = useLoginData({}); + const setErrorBannerTextAtom = useSetAtom(errorBannerTextAtom); + const cardDisabled = cardSellOrders.length === 0; const { marketplaceClient, ecocreditClient } = useLedger(); @@ -94,6 +97,16 @@ export const BuyCreditsForm = ({ }), ); + const stripePromise = loadStripe(stripeKey); + const stripeOptions = useMemo( + () => ({ + amount: (data?.[CURRENCY_AMOUNT] ?? 0) * 100, // stripe amounts should be in the smallest currency unit (e.g., 100 cents to charge $1.00), + currency: USD_DENOM, + ...defaultStripeOptions, + }), + [data], + ); + return (
@@ -115,20 +128,22 @@ export const BuyCreditsForm = ({ /> )} {activeStep === 1 && ( - {}} - amount={(data?.[CURRENCY_AMOUNT] ?? 0) * 100} // stripe amounts should be in the smallest currency unit (e.g., 100 cents to charge $1.00) - currency={USD_DENOM} - login={onButtonClick} - retiring={retiring} - stripePublishableKey={import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY} - wallet={wallet} - accountEmail={privActiveAccount?.email} - accountName={activeAccount?.name} - accountId={activeAccount?.id} - paymentMethods={paymentMethodData?.paymentMethods} - /> + + { + handleSaveNext({ ...values }); + }} + login={onButtonClick} + retiring={retiring} + wallet={wallet} + accountEmail={privActiveAccount?.email} + accountName={activeAccount?.name} + accountId={activeAccount?.id} + paymentMethods={paymentMethodData?.paymentMethods} + setError={setErrorBannerTextAtom} + /> + )} {activeStep === 2 && ( Date: Mon, 23 Sep 2024 13:01:58 +0200 Subject: [PATCH 05/10] refactor: address review comments and add PaymentInfoFormFiat --- .../CreditsAmount/CreditsAmount.test.tsx | 1 - .../molecules/CreditsAmount/CreditsAmount.tsx | 10 +-- .../CreditsAmount/CreditsAmount.utils.tsx | 15 +++-- .../PaymentInfoForm.CustomerInfo.tsx | 4 +- .../PaymentInfoForm.stories.tsx | 7 +-- .../PaymentInfoForm/PaymentInfoForm.tsx | 20 +++--- .../PaymentInfoForm/PaymentInfoFormFiat.tsx | 12 ++++ .../ProjectDetails/hooks/useGetProject.ts | 6 +- .../src/pages/BuyCredits/BuyCredits.Form.tsx | 63 +++++++++++++------ .../src/pages/BuyCredits/BuyCredits.tsx | 17 ++--- .../src/pages/BuyCredits/BuyCredits.utils.ts | 14 +++++ 11 files changed, 108 insertions(+), 61 deletions(-) create mode 100644 web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoFormFiat.tsx diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.test.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.test.tsx index e14e0d695c..5f63e49753 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.test.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.test.tsx @@ -95,7 +95,6 @@ describe('CreditsAmount', () => { const currencyInput = screen.getByLabelText(/Currency Input/i); await userEvent.click(maxCreditsButton); - screen.debug(); expect(creditsInput).toHaveValue(1125); expect(currencyInput).toHaveValue(3185); diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx index 8bf3df214b..e363d7757e 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx @@ -1,12 +1,11 @@ import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; -import { useFormContext, useWatch } from 'react-hook-form'; +import { useFormContext } from 'react-hook-form'; import { Trans } from '@lingui/macro'; import { ChooseCreditsFormSchemaType } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema'; -import { denomToMicro, microToDenom } from 'lib/denom.utils'; +import { microToDenom } from 'lib/denom.utils'; import { PAYMENT_OPTIONS } from 'pages/BuyCredits/BuyCredits.constants'; -import { CardSellOrder } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm.types'; import { findDisplayDenom } from '../DenomLabel/DenomLabel.utils'; import { @@ -56,7 +55,7 @@ export const CreditsAmount = ({ [card, cardSellOrders, filteredCryptoSellOrders], ); - console.log('orderedSellOrders', orderedSellOrders); + useEffect(() => { // Reset amounts to 0 on currency change setValue(CREDITS_AMOUNT, 0); @@ -140,11 +139,12 @@ export const CreditsAmount = ({ value, card, orderedSellOrders, + creditTypePrecision, }); setValue(CREDITS_AMOUNT, currentCreditsAmount); setValue(SELL_ORDERS, sellOrders); }, - [card, orderedSellOrders, setValue], + [card, orderedSellOrders, setValue, creditTypePrecision], ); const displayDenom = findDisplayDenom({ diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx index 031e87a33e..a312270c2f 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx @@ -43,11 +43,13 @@ type GetCreditsAmountParams = { value: number; card: boolean; orderedSellOrders: UISellOrderInfo[]; + creditTypePrecision?: number | null; }; export const getCreditsAmount = ({ value, card, orderedSellOrders, + creditTypePrecision, }: GetCreditsAmountParams) => { const currentCurrencyAmount = card ? value : denomToMicro(value); let currentCreditsAmount = 0; @@ -78,7 +80,12 @@ export const getCreditsAmount = ({ break; } } - return { currentCreditsAmount, sellOrders }; + return { + currentCreditsAmount: parseFloat( + currentCreditsAmount.toFixed(creditTypePrecision || 6), + ), + sellOrders, + }; }; type GetCurrencyAmountParams = { @@ -121,11 +128,11 @@ export const getCurrencyAmount = ({ break; } } - console.log('currentCurrencyAmount', microToDenom(currentCurrencyAmount)); + return { [CURRENCY_AMOUNT]: card - ? currentCurrencyAmount - : microToDenom(currentCurrencyAmount), + ? parseFloat(currentCurrencyAmount.toFixed(6)) + : parseFloat(microToDenom(currentCurrencyAmount).toFixed(6)), sellOrders, }; }; diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx index e2b0426060..b9d02b6651 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx @@ -69,7 +69,7 @@ export const CustomerInfo = ({ /> )} - {!accountId && !wallet && ( + {!accountId && !wallet?.address && ( { const options = { amount: 1000, currency: 'usd', ...defaultStripeOptions }; - const stripePromise = - args.paymentOption === PAYMENT_OPTIONS.CARD && - stripeKey && - loadStripe(stripeKey); + const stripePromise = loadStripe(stripeKey); return ( diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx index 6939e0b484..65ff43bf7e 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx @@ -1,9 +1,10 @@ import { useEffect } from 'react'; import { useFormState } from 'react-hook-form'; import { useLingui } from '@lingui/react'; -import { useElements, useStripe } from '@stripe/react-stripe-js'; +import { Stripe, StripeElements } from '@stripe/stripe-js'; import { PrevNextButtons } from 'web-components/src/components/molecules/PrevNextButtons/PrevNextButtons'; +import { UseStateSetter } from 'web-components/src/types/react/useState'; import { NEXT, PAYMENT_OPTIONS } from 'pages/BuyCredits/BuyCredits.constants'; import { PaymentOptionsType } from 'pages/BuyCredits/BuyCredits.types'; @@ -23,10 +24,11 @@ import { export type PaymentInfoFormProps = { paymentOption: PaymentOptionsType; - onSubmit: ( - values: PaymentInfoFormSchemaType & { confirmationTokenId?: string }, - ) => Promise; + onSubmit: (values: PaymentInfoFormSchemaType) => Promise; setError: (error: string) => void; + setConfirmationTokenId: UseStateSetter; + stripe?: Stripe | null; + elements?: StripeElements | null; } & CustomerInfoProps & PaymentInfoProps; @@ -41,11 +43,12 @@ export const PaymentInfoForm = ({ paymentMethods, setError, retiring, + setConfirmationTokenId, + stripe, + elements, }: PaymentInfoFormProps) => { const { _ } = useLingui(); const { handleBack } = useMultiStep(); - const stripe = useStripe(); - const elements = useElements(); const form = useZodForm({ schema: paymentInfoFormSchema(paymentOption), @@ -78,7 +81,6 @@ export const PaymentInfoForm = ({ if (card && !stripe) { return; } - let confirmationTokenId: string | undefined; if (card && stripe && elements && !values.paymentMethodId) { const submitRes = await elements?.submit(); if (submitRes?.error?.message) { @@ -102,9 +104,9 @@ export const PaymentInfoForm = ({ setError(error?.message); return; } - confirmationTokenId = confirmationToken?.id; + setConfirmationTokenId(confirmationToken?.id); } - onSubmit({ confirmationTokenId, ...values }); + onSubmit(values); }} >
diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoFormFiat.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoFormFiat.tsx new file mode 100644 index 0000000000..88f5f0633d --- /dev/null +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoFormFiat.tsx @@ -0,0 +1,12 @@ +import { useElements, useStripe } from '@stripe/react-stripe-js'; + +import { PaymentInfoForm, PaymentInfoFormProps } from './PaymentInfoForm'; + +type Props = Omit; + +export const PaymentInfoFormFiat = (props: Props) => { + const stripe = useStripe(); + const elements = useElements(); + + return ; +}; diff --git a/web-marketplace/src/components/templates/ProjectDetails/hooks/useGetProject.ts b/web-marketplace/src/components/templates/ProjectDetails/hooks/useGetProject.ts index 96da534ae7..d947c5bfc7 100644 --- a/web-marketplace/src/components/templates/ProjectDetails/hooks/useGetProject.ts +++ b/web-marketplace/src/components/templates/ProjectDetails/hooks/useGetProject.ts @@ -19,9 +19,9 @@ export const useGetProject = () => { const graphqlClient = useApolloClient(); const { ecocreditClient } = useLedger(); - // first, check if projectId is an off-chain project handle (for legacy projects like "wilmot") - // or an chain project id - // or and off-chain project with an UUID + // First, check if projectId is an on-chain project id + // or an off-chain project UUID. + // If neither of those, it's a project slug. const isOnChainId = getIsOnChainId(projectId); const isOffChainUuid = getIsUuid(projectId); diff --git a/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx b/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx index 7e1e8fa246..13380acebe 100644 --- a/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx +++ b/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx @@ -27,6 +27,7 @@ import { LoginFlow } from 'components/organisms/LoginFlow/LoginFlow'; import { PaymentInfoForm } from 'components/organisms/PaymentInfoForm/PaymentInfoForm'; import { defaultStripeOptions } from 'components/organisms/PaymentInfoForm/PaymentInfoForm.constants'; import { PaymentInfoFormSchemaType } from 'components/organisms/PaymentInfoForm/PaymentInfoForm.schema'; +import { PaymentInfoFormFiat } from 'components/organisms/PaymentInfoForm/PaymentInfoFormFiat'; import { useMultiStep } from 'components/templates/MultiStepTemplate'; import { PAYMENT_OPTIONS, stripeKey } from './BuyCredits.constants'; @@ -37,6 +38,7 @@ type Props = { setPaymentOption: UseStateSetter; retiring: boolean; setRetiring: UseStateSetter; + setConfirmationTokenId: UseStateSetter; cardSellOrders: Array; cryptoSellOrders: Array; creditTypeAbbrev?: string; @@ -47,6 +49,7 @@ export const BuyCreditsForm = ({ setPaymentOption, retiring, setRetiring, + setConfirmationTokenId, cardSellOrders, cryptoSellOrders, creditTypeAbbrev, @@ -54,9 +57,8 @@ export const BuyCreditsForm = ({ }: Props) => { const { data, activeStep, handleSaveNext } = useMultiStep< Partial & - Partial & { - confirmationTokenId?: string; - } & Partial + Partial & + Partial >(); const { wallet } = useWallet(); const { activeAccount, privActiveAccount } = useAuth(); @@ -100,7 +102,7 @@ export const BuyCreditsForm = ({ const stripePromise = loadStripe(stripeKey); const stripeOptions = useMemo( () => ({ - amount: (data?.[CURRENCY_AMOUNT] ?? 0) * 100, // stripe amounts should be in the smallest currency unit (e.g., 100 cents to charge $1.00), + amount: (data?.[CURRENCY_AMOUNT] || 0) * 100, // stripe amounts should be in the smallest currency unit (e.g., 100 cents to charge $1.00), currency: USD_DENOM, ...defaultStripeOptions, }), @@ -128,22 +130,43 @@ export const BuyCreditsForm = ({ /> )} {activeStep === 1 && ( - - { - handleSaveNext({ ...values }); - }} - login={onButtonClick} - retiring={retiring} - wallet={wallet} - accountEmail={privActiveAccount?.email} - accountName={activeAccount?.name} - accountId={activeAccount?.id} - paymentMethods={paymentMethodData?.paymentMethods} - setError={setErrorBannerTextAtom} - /> - + <> + {paymentOption === PAYMENT_OPTIONS.CARD ? ( + + { + handleSaveNext(values); + }} + login={onButtonClick} + retiring={retiring} + wallet={wallet} + accountEmail={privActiveAccount?.email} + accountName={activeAccount?.name} + accountId={activeAccount?.id} + paymentMethods={paymentMethodData?.paymentMethods} + setError={setErrorBannerTextAtom} + setConfirmationTokenId={setConfirmationTokenId} + /> + + ) : ( + { + handleSaveNext(values); + }} + login={onButtonClick} + retiring={retiring} + wallet={wallet} + accountEmail={privActiveAccount?.email} + accountName={activeAccount?.name} + accountId={activeAccount?.id} + paymentMethods={paymentMethodData?.paymentMethods} + setError={setErrorBannerTextAtom} + setConfirmationTokenId={setConfirmationTokenId} + /> + )} + )} {activeStep === 2 && ( { const { _ } = useLingui(); @@ -37,6 +35,9 @@ export const BuyCredits = () => { : PAYMENT_OPTIONS.CRYPTO, ); const [retiring, setRetiring] = useState(true); + const [confirmationTokenId, setConfirmationTokenId] = useState< + string | undefined + >(); const formModel = getFormModel({ _, paymentOption, retiring }); const sellOrders = useMemo( @@ -45,13 +46,7 @@ export const BuyCredits = () => { ); const cardSellOrders = useMemo( - () => - sanityProject?.fiatSellOrders?.map(fiatOrder => ({ - ...fiatOrder, - ...sellOrders.filter( - cryptoOrder => cryptoOrder.id.toString() === fiatOrder?.sellOrderId, - )?.[0], - })) || [], + () => getCardSellOrders(sanityProject?.fiatSellOrders, sellOrders), [sanityProject?.fiatSellOrders, sellOrders], ); @@ -66,7 +61,6 @@ export const BuyCredits = () => { formId={formModel.formId} steps={formModel.steps} initialValues={{}} - useLocalStorage={false} > { offChainProject?.onChainId ?? onChainProjectId }`} + setConfirmationTokenId={setConfirmationTokenId} /> )} diff --git a/web-marketplace/src/pages/BuyCredits/BuyCredits.utils.ts b/web-marketplace/src/pages/BuyCredits/BuyCredits.utils.ts index afe027aa30..f521263d85 100644 --- a/web-marketplace/src/pages/BuyCredits/BuyCredits.utils.ts +++ b/web-marketplace/src/pages/BuyCredits/BuyCredits.utils.ts @@ -1,7 +1,10 @@ import { msg } from '@lingui/macro'; +import { Project } from 'generated/sanity-graphql'; import { TranslatorType } from 'lib/i18n/i18n.types'; +import { UISellOrderInfo } from 'pages/Projects/AllProjects/AllProjects.types'; + import { PAYMENT_OPTIONS } from './BuyCredits.constants'; import { PaymentOptionsType } from './BuyCredits.types'; @@ -37,3 +40,14 @@ export const getFormModel = ({ ], }; }; + +export const getCardSellOrders = ( + sanityFiatSellOrders: Project['fiatSellOrders'], + sellOrders: UISellOrderInfo[], +) => + sanityFiatSellOrders?.map(fiatOrder => ({ + ...fiatOrder, + ...sellOrders.filter( + cryptoOrder => cryptoOrder.id.toString() === fiatOrder?.sellOrderId, + )?.[0], + })) || []; From 34e251ffd65856e6ebd3a88f8e0109fdac7e8e9c Mon Sep 17 00:00:00 2001 From: blushi Date: Mon, 23 Sep 2024 15:26:56 +0200 Subject: [PATCH 06/10] fix: ChooseCreditsForm validation --- .../molecules/CreditsAmount/CreditsAmount.tsx | 10 +++++-- .../CreditsAmount/CreditsAmount.types.tsx | 1 - .../molecules/CreditsAmount/CreditsInput.tsx | 30 ++++--------------- .../molecules/CreditsAmount/CurrencyInput.tsx | 19 ++---------- .../ChooseCreditsForm/ChooseCreditsForm.tsx | 27 ++++++++--------- .../PaymentInfoForm.CustomerInfo.tsx | 2 +- .../organisms/PostForm/PostForm.tsx | 2 +- .../src/pages/BuyCredits/BuyCredits.Form.tsx | 6 ++-- 8 files changed, 33 insertions(+), 64 deletions(-) diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx index e363d7757e..88a48b5cd3 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx @@ -3,6 +3,8 @@ import { useFormContext } from 'react-hook-form'; import { Trans } from '@lingui/macro'; import { ChooseCreditsFormSchemaType } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema'; +import TextField from 'web-components/src/components/inputs/new/TextField/TextField'; + import { microToDenom } from 'lib/denom.utils'; import { PAYMENT_OPTIONS } from 'pages/BuyCredits/BuyCredits.constants'; @@ -42,7 +44,7 @@ export const CreditsAmount = ({ creditTypePrecision, }: CreditsAmountProps) => { const [maxCreditsSelected, setMaxCreditsSelected] = useState(false); - const { setValue } = useFormContext(); + const { setValue, trigger } = useFormContext(); const card = paymentOption === PAYMENT_OPTIONS.CARD; const orderedSellOrders = useMemo( @@ -60,7 +62,8 @@ export const CreditsAmount = ({ // Reset amounts to 0 on currency change setValue(CREDITS_AMOUNT, 0); setValue(CURRENCY_AMOUNT, 0); - }, [currency, setValue]); + trigger(); + }, [currency, setValue, trigger]); useEffect(() => { setSpendingCap( @@ -100,6 +103,7 @@ export const CreditsAmount = ({ return formatFullSellOrder({ order, card, price }); }), ); + trigger(); setMaxCreditsSelected(false); } }, [ @@ -110,6 +114,7 @@ export const CreditsAmount = ({ paymentOption, setValue, spendingCap, + trigger, ]); // Credits amount change @@ -178,7 +183,6 @@ export const CreditsAmount = ({
{paymentOption === PAYMENT_OPTIONS.CRYPTO && ( diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx index c6a6ba1d39..6f97eafd19 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx @@ -31,7 +31,6 @@ export interface CreditsAmountProps { export interface CreditsInputProps { creditsAvailable: number; handleCreditsAmountChange: (e: ChangeEvent) => void; - paymentOption: PaymentOptionsType; } export interface CurrencyInputProps { diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsInput.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsInput.tsx index 9acbe4ce9e..1f120d454b 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsInput.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsInput.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useEffect, useState } from 'react'; +import { ChangeEvent } from 'react'; import { useFormContext } from 'react-hook-form'; import { Trans } from '@lingui/macro'; import { ChooseCreditsFormSchemaType } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema'; @@ -12,27 +12,12 @@ import { CreditsInputProps } from './CreditsAmount.types'; export const CreditsInput = ({ creditsAvailable, handleCreditsAmountChange, - paymentOption, }: CreditsInputProps) => { - const [maxCreditsAvailable, setMaxCreditsAvailable] = - useState(creditsAvailable); - const [isFocused, setIsFocused] = useState(false); const { - setValue, register, formState: { errors }, } = useFormContext(); - const { onChange, onBlur, name, ref } = register(CREDITS_AMOUNT); - - const onHandleFocus = () => setIsFocused(true); - const onHandleBlur = (event: { target: any; type?: any }) => { - setIsFocused(false); - onBlur(event); - }; - - useEffect(() => { - setMaxCreditsAvailable(creditsAvailable); - }, [creditsAvailable, paymentOption, setValue]); + const { onChange } = register(CREDITS_AMOUNT); const onHandleChange = (event: ChangeEvent) => { onChange(event); @@ -42,21 +27,16 @@ export const CreditsInput = ({ return (
(); - const { onChange, onBlur, name, ref } = register(CURRENCY_AMOUNT); - const [isFocused, setIsFocused] = useState(false); + const { onChange } = register(CURRENCY_AMOUNT); - const handleOnFocus = () => setIsFocused(true); - const handleOnBlur = (event: { target: any; type?: any }) => { - setIsFocused(false); - onBlur(event); - }; const handleOnChange = useCallback( (event: ChangeEvent) => { onChange(event); @@ -72,17 +66,10 @@ export const CurrencyInput = ({ $ )} @@ -142,6 +139,7 @@ export function ChooseCreditsForm({ // Reset amounts to 0 on retirement change form.setValue(CREDITS_AMOUNT, 0); form.setValue(CURRENCY_AMOUNT, 0); + form.trigger(); // trigger validation }, [form, setRetiring]); const handlePaymentOptions = useCallback( @@ -151,6 +149,7 @@ export function ChooseCreditsForm({ setCurrency( option === PAYMENT_OPTIONS.CARD ? cardCurrency : defaultCryptoCurrency, ); + form.trigger(); }, [cardCurrency, defaultCryptoCurrency, form, setPaymentOption], ); @@ -230,7 +229,7 @@ export function ChooseCreditsForm({
{ navigate(projectHref); diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx index b9d02b6651..ab8b1f2fb5 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CustomerInfo.tsx @@ -95,7 +95,7 @@ export const CustomerInfo = ({ error={!!errors['email']} helperText={errors['email']?.message} disabled={!!accountEmail} - optional={!!wallet} + optional={!!wallet?.address} /> {!accountId && !wallet?.address && ( { - handleSaveNext(values); + handleSaveNext({ ...data, ...values }); }} allowedDenoms={allowedDenomsData?.allowedDenoms} creditTypePrecision={creditTypeData?.creditType?.precision} @@ -136,7 +136,7 @@ export const BuyCreditsForm = ({ { - handleSaveNext(values); + handleSaveNext({ ...data, ...values }); }} login={onButtonClick} retiring={retiring} @@ -153,7 +153,7 @@ export const BuyCreditsForm = ({ { - handleSaveNext(values); + handleSaveNext({ ...data, ...values }); }} login={onButtonClick} retiring={retiring} From 63ea185bf2c9bfbefc13958c7f9997173d304328 Mon Sep 17 00:00:00 2001 From: blushi Date: Mon, 23 Sep 2024 17:00:33 +0200 Subject: [PATCH 07/10] feat: use initial values on ChooseCreditsForm --- .../CreditsAmount/CreditsAmount.constants.ts | 1 + .../CreditsAmount/CreditsAmount.stories.tsx | 5 +- .../CreditsAmount/CreditsAmount.test.tsx | 2 - .../molecules/CreditsAmount/CreditsAmount.tsx | 23 +++----- .../CreditsAmount/CreditsAmount.types.tsx | 5 +- .../molecules/CreditsAmount/CurrencyInput.tsx | 38 +++++++++---- .../ChooseCreditsForm.schema.tsx | 5 ++ .../ChooseCreditsForm.stories.tsx | 2 + .../ChooseCreditsForm.test.tsx | 2 +- .../ChooseCreditsForm/ChooseCreditsForm.tsx | 55 ++++++++----------- .../src/pages/BuyCredits/BuyCredits.Form.tsx | 47 ++++++++++++++-- 11 files changed, 110 insertions(+), 75 deletions(-) diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.constants.ts b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.constants.ts index 9893334528..37e2bc14c3 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.constants.ts +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.constants.ts @@ -4,6 +4,7 @@ import { CURRENCIES } from 'components/molecules/DenomIconWithCurrency/DenomIcon export const CREDITS_AMOUNT = 'creditsAmount'; export const CURRENCY_AMOUNT = 'currencyAmount'; +export const CURRENCY = 'currency'; export const CREDIT_VINTAGE_OPTIONS = 'creditVintageOptions'; export const SELL_ORDERS = 'sellOrders'; diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.stories.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.stories.tsx index 4d715ddb94..740c51b184 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.stories.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.stories.tsx @@ -30,7 +30,6 @@ const CreditsWithForm = (args: any) => { args.paymentOption === PAYMENT_OPTIONS.CARD ? { askDenom: CURRENCIES.usd, askBaseDenom: CURRENCIES.usd } : defaultCryptoCurrency; - const [currency, setCurrency] = useState(initCurrency); const [spendingCap, setSpendingCap] = useState(0); const [creditsAvailable, setCreditsAvailable] = useState(0); @@ -47,14 +46,12 @@ const CreditsWithForm = (args: any) => { mode: 'onChange', }); const filteredCryptoSellOrders = cryptoSellOrders.filter( - order => order.askDenom === currency.askDenom, + order => order.askDenom === initCurrency.askDenom, ); return ( { const formDefaultValues = { paymentOption: PAYMENT_OPTIONS.CARD, - currency: { askDenom: CURRENCIES.usd, askBaseDenom: CURRENCIES.usd }, - setCurrency: () => {}, spendingCap: 3185, setSpendingCap: () => {}, creditsAvailable: 1125, diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx index 88a48b5cd3..bf2102c85c 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx @@ -1,10 +1,8 @@ import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; -import { useFormContext } from 'react-hook-form'; +import { useFormContext, useWatch } from 'react-hook-form'; import { Trans } from '@lingui/macro'; import { ChooseCreditsFormSchemaType } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema'; -import TextField from 'web-components/src/components/inputs/new/TextField/TextField'; - import { microToDenom } from 'lib/denom.utils'; import { PAYMENT_OPTIONS } from 'pages/BuyCredits/BuyCredits.constants'; @@ -12,6 +10,7 @@ import { PAYMENT_OPTIONS } from 'pages/BuyCredits/BuyCredits.constants'; import { findDisplayDenom } from '../DenomLabel/DenomLabel.utils'; import { CREDITS_AMOUNT, + CURRENCY, CURRENCY_AMOUNT, SELL_ORDERS, } from './CreditsAmount.constants'; @@ -30,8 +29,6 @@ import { CurrencyInput } from './CurrencyInput'; export const CreditsAmount = ({ paymentOption, - currency, - setCurrency, creditsAvailable, setCreditsAvailable, filteredCryptoSellOrders, @@ -44,7 +41,12 @@ export const CreditsAmount = ({ creditTypePrecision, }: CreditsAmountProps) => { const [maxCreditsSelected, setMaxCreditsSelected] = useState(false); - const { setValue, trigger } = useFormContext(); + const { setValue, trigger, control } = + useFormContext(); + const currency = useWatch({ + control, + name: CURRENCY, + }); const card = paymentOption === PAYMENT_OPTIONS.CARD; const orderedSellOrders = useMemo( @@ -58,13 +60,6 @@ export const CreditsAmount = ({ [card, cardSellOrders, filteredCryptoSellOrders], ); - useEffect(() => { - // Reset amounts to 0 on currency change - setValue(CREDITS_AMOUNT, 0); - setValue(CURRENCY_AMOUNT, 0); - trigger(); - }, [currency, setValue, trigger]); - useEffect(() => { setSpendingCap( getSpendingCap(paymentOption, filteredCryptoSellOrders, cardSellOrders), @@ -172,8 +167,6 @@ export const CreditsAmount = ({ maxCurrencyAmount={spendingCap} paymentOption={paymentOption} defaultCryptoCurrency={defaultCryptoCurrency} - currency={currency} - setCurrency={setCurrency} handleCurrencyAmountChange={handleCurrencyAmountChange} cryptoCurrencies={cryptoCurrencies} displayDenom={displayDenom} diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx index 6f97eafd19..2ce9b7aa5c 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx @@ -5,6 +5,7 @@ import { UseStateSetter } from 'web-components/src/types/react/useState'; import { PaymentOptionsType } from 'pages/BuyCredits/BuyCredits.types'; import { UISellOrderInfo } from 'pages/Projects/AllProjects/AllProjects.types'; +import { ChooseCreditsFormSchemaType } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema'; import { AllowedDenoms } from '../DenomLabel/DenomLabel.utils'; @@ -14,8 +15,6 @@ export type Currency = { }; export interface CreditsAmountProps { paymentOption: PaymentOptionsType; - currency: Currency; - setCurrency: (currency: Currency) => void; spendingCap: number; setSpendingCap: UseStateSetter; creditsAvailable: number; @@ -37,8 +36,6 @@ export interface CurrencyInputProps { maxCurrencyAmount: number; paymentOption: PaymentOptionsType; defaultCryptoCurrency: Currency; - currency: Currency; - setCurrency: (currency: Currency) => void; handleCurrencyAmountChange: (e: ChangeEvent) => void; cryptoCurrencies: Currency[]; allowedDenoms?: AllowedDenoms; diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CurrencyInput.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CurrencyInput.tsx index fb6afcdc2a..b93c465d4f 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CurrencyInput.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CurrencyInput.tsx @@ -1,5 +1,5 @@ import { ChangeEvent, lazy, useCallback, useState } from 'react'; -import { useFormContext } from 'react-hook-form'; +import { useFormContext, useWatch } from 'react-hook-form'; import { ChooseCreditsFormSchemaType } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema'; import TextField from 'web-components/src/components/inputs/new/TextField/TextField'; @@ -9,7 +9,11 @@ import { DenomIconWithCurrency } from 'components/molecules/DenomIconWithCurrenc import { CURRENCIES } from 'components/molecules/DenomIconWithCurrency/DenomIconWithCurrency.constants'; import { findDisplayDenom } from '../DenomLabel/DenomLabel.utils'; -import { CURRENCY_AMOUNT } from './CreditsAmount.constants'; +import { + CREDITS_AMOUNT, + CURRENCY, + CURRENCY_AMOUNT, +} from './CreditsAmount.constants'; import { CurrencyInputProps } from './CreditsAmount.types'; const CustomSelect = lazy( @@ -23,8 +27,6 @@ export const CurrencyInput = ({ maxCurrencyAmount, paymentOption, defaultCryptoCurrency, - currency, - setCurrency, handleCurrencyAmountChange, cryptoCurrencies, displayDenom, @@ -33,9 +35,17 @@ export const CurrencyInput = ({ const { register, formState: { errors }, + setValue, + control, + trigger, } = useFormContext(); const { onChange } = register(CURRENCY_AMOUNT); + const currency = useWatch({ + control, + name: CURRENCY, + }); + const handleOnChange = useCallback( (event: ChangeEvent) => { onChange(event); @@ -46,7 +56,8 @@ export const CurrencyInput = ({ const onHandleCurrencyChange = useCallback( (askDenom: string) => { - setCurrency( + setValue( + CURRENCY, askDenom === CURRENCIES.usd ? { askDenom: CURRENCIES.usd, askBaseDenom: CURRENCIES.usd } : { @@ -56,8 +67,11 @@ export const CurrencyInput = ({ )?.[0].askBaseDenom, }, ); + setValue(CREDITS_AMOUNT, 0); + setValue(CURRENCY_AMOUNT, 0); + trigger(); }, - [cryptoCurrencies, setCurrency], + [cryptoCurrencies, setValue, trigger], ); return ( @@ -115,23 +129,23 @@ export const CurrencyInput = ({ /> ) : ( ({ + options={cryptoCurrencies.map(cur => ({ component: { - label: currency.askDenom, + label: cur.askDenom, element: () => ( ), }, }))} onSelect={onHandleCurrencyChange} - defaultOption={defaultCryptoCurrency.askDenom} + defaultOption={currency.askDenom} /> ) } diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema.tsx index c6df095bcd..38c6065e34 100644 --- a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema.tsx +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema.tsx @@ -2,6 +2,7 @@ import { i18n } from '@lingui/core'; import { CREDIT_VINTAGE_OPTIONS, CREDITS_AMOUNT, + CURRENCY, CURRENCY_AMOUNT, SELL_ORDERS, } from 'web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.constants'; @@ -46,6 +47,10 @@ export const createChooseCreditsFormSchema = ({ }), ), [CREDIT_VINTAGE_OPTIONS]: z.array(z.string()), + [CURRENCY]: z.object({ + askDenom: z.string(), + askBaseDenom: z.string(), + }), }); }; diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.stories.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.stories.tsx index 23f11f9728..084dbfb404 100644 --- a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.stories.tsx +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.stories.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { action } from '@storybook/addon-actions'; import { Meta, StoryObj } from '@storybook/react'; import { allowedDenoms, @@ -41,4 +42,5 @@ ChooseCredits.args = { cryptoSellOrders, cardSellOrders, allowedDenoms, + onPrev: action('prev'), }; diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.test.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.test.tsx index c896b90159..bf5b047524 100644 --- a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.test.tsx +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.test.tsx @@ -16,12 +16,12 @@ describe('ChooseCreditsForm', () => { setPaymentOption: () => {}, retiring: true, setRetiring: () => {}, + onPrev: () => {}, onSubmit: async (values: ChooseCreditsFormSchemaType) => {}, cardSellOrders, cryptoSellOrders, cardDisabled: false, allowedDenoms, - projectHref: '/lorem', }; it('renders without crashing', () => { render(); diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx index d15ea6c129..ab44c3fe95 100644 --- a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx @@ -1,12 +1,11 @@ import { Suspense, useCallback, useEffect, useMemo, useState } from 'react'; -import { useFormState } from 'react-hook-form'; -import { useNavigate } from 'react-router-dom'; -import { msg } from '@lingui/macro'; +import { useFormState, useWatch } from 'react-hook-form'; import { useLingui } from '@lingui/react'; import { CreditsAmount } from 'web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount'; import { CREDIT_VINTAGE_OPTIONS, CREDITS_AMOUNT, + CURRENCY, CURRENCY_AMOUNT, SELL_ORDERS, } from 'web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.constants'; @@ -45,8 +44,8 @@ export type Props = { cardDisabled: boolean; allowedDenoms?: AllowedDenoms; creditTypePrecision?: number | null; - projectHref: string; initialValues?: ChooseCreditsFormSchemaType; + onPrev: () => void; }; export function ChooseCreditsForm({ @@ -60,12 +59,10 @@ export function ChooseCreditsForm({ cardDisabled, allowedDenoms, creditTypePrecision, - projectHref, initialValues, + onPrev, }: Props) { const { _ } = useLingui(); - const navigate = useNavigate(); - const cryptoCurrencies = useMemo( () => cryptoSellOrders @@ -90,19 +87,6 @@ export function ChooseCreditsForm({ [], ); - const [currency, setCurrency] = useState(undefined); - useEffect( - () => - setCurrency( - prev => - prev || - (paymentOption === PAYMENT_OPTIONS.CARD - ? cardCurrency - : defaultCryptoCurrency), - ), - [cardCurrency, defaultCryptoCurrency, paymentOption], - ); - const [spendingCap, setSpendingCap] = useState(0); const [creditsAvailable, setCreditsAvailable] = useState(0); @@ -111,11 +95,16 @@ export function ChooseCreditsForm({ creditsAvailable, spendingCap, }), - defaultValues: initialValues || { - [CURRENCY_AMOUNT]: 0, - [CREDITS_AMOUNT]: 0, - [SELL_ORDERS]: [], - [CREDIT_VINTAGE_OPTIONS]: [], + defaultValues: { + [CURRENCY_AMOUNT]: initialValues?.[CURRENCY_AMOUNT] || 0, + [CREDITS_AMOUNT]: initialValues?.[CREDITS_AMOUNT] || 0, + [SELL_ORDERS]: initialValues?.[SELL_ORDERS] || [], + [CREDIT_VINTAGE_OPTIONS]: initialValues?.[CREDIT_VINTAGE_OPTIONS] || [], + [CURRENCY]: initialValues?.[CURRENCY]?.askDenom + ? initialValues?.[CURRENCY] + : paymentOption === PAYMENT_OPTIONS.CARD + ? cardCurrency + : defaultCryptoCurrency, }, mode: 'onChange', }); @@ -123,6 +112,11 @@ export function ChooseCreditsForm({ control: form.control, }); + const currency = useWatch({ + control: form.control, + name: CURRENCY, + }); + const filteredCryptoSellOrders = useMemo( () => getFilteredCryptoSellOrders({ @@ -146,9 +140,12 @@ export function ChooseCreditsForm({ (option: string) => { setPaymentOption(option as PaymentOptionsType); form.setValue(CREDIT_VINTAGE_OPTIONS, []); - setCurrency( + form.setValue( + CURRENCY, option === PAYMENT_OPTIONS.CARD ? cardCurrency : defaultCryptoCurrency, ); + form.setValue(CREDITS_AMOUNT, 0); + form.setValue(CURRENCY_AMOUNT, 0); form.trigger(); }, [cardCurrency, defaultCryptoCurrency, form, setPaymentOption], @@ -196,8 +193,6 @@ export function ChooseCreditsForm({ {currency && ( { - navigate(projectHref); - }} + onPrev={onPrev} />
diff --git a/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx b/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx index a06b3a751f..632f9d4258 100644 --- a/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx +++ b/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx @@ -1,5 +1,6 @@ -import { useMemo } from 'react'; -import { Elements, useElements, useStripe } from '@stripe/react-stripe-js'; +import { useEffect, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Elements } from '@stripe/react-stripe-js'; import { loadStripe } from '@stripe/stripe-js'; import { useQuery } from '@tanstack/react-query'; import { USD_DENOM } from 'config/allowedBaseDenoms'; @@ -16,10 +17,18 @@ import { getPaymentMethodsQuery } from 'lib/queries/react-query/registry-server/ import { useWallet } from 'lib/wallet/wallet'; import { UISellOrderInfo } from 'pages/Projects/AllProjects/AllProjects.types'; -import { CURRENCY_AMOUNT } from 'components/molecules/CreditsAmount/CreditsAmount.constants'; +import { + CREDIT_VINTAGE_OPTIONS, + CREDITS_AMOUNT, + CURRENCY, + CURRENCY_AMOUNT, + SELL_ORDERS, +} from 'components/molecules/CreditsAmount/CreditsAmount.constants'; +import { Currency } from 'components/molecules/CreditsAmount/CreditsAmount.types'; import { AgreePurchaseForm } from 'components/organisms/AgreePurchaseForm/AgreePurchaseForm'; import { AgreePurchaseFormSchemaType } from 'components/organisms/AgreePurchaseForm/AgreePurchaseForm.schema'; import { ChooseCreditsForm } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm'; +import { PaymentOptions } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm.PaymentOptions'; import { ChooseCreditsFormSchemaType } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema'; import { CardSellOrder } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm.types'; import { useLoginData } from 'components/organisms/LoginButton/hooks/useLoginData'; @@ -56,7 +65,10 @@ export const BuyCreditsForm = ({ projectHref, }: Props) => { const { data, activeStep, handleSaveNext } = useMultiStep< - Partial & + { + paymentOption?: PaymentOptionsType; + retiring?: boolean; + } & Partial & Partial & Partial >(); @@ -69,6 +81,7 @@ export const BuyCreditsForm = ({ walletsUiConfig, onButtonClick, } = useLoginData({}); + const navigate = useNavigate(); const setErrorBannerTextAtom = useSetAtom(errorBannerTextAtom); @@ -109,6 +122,13 @@ export const BuyCreditsForm = ({ [data], ); + useEffect(() => { + setRetiring(prev => + typeof data?.retiring === 'undefined' ? prev : data?.retiring, + ); + setPaymentOption(prev => data?.paymentOption || prev); + }, [data, setPaymentOption, setRetiring]); + return (
@@ -122,11 +142,26 @@ export const BuyCreditsForm = ({ cardSellOrders={cardSellOrders} cryptoSellOrders={cryptoSellOrders} onSubmit={async (values: ChooseCreditsFormSchemaType) => { - handleSaveNext({ ...data, ...values }); + handleSaveNext({ + ...data, + ...values, + retiring, + paymentOption, + }); }} allowedDenoms={allowedDenomsData?.allowedDenoms} creditTypePrecision={creditTypeData?.creditType?.precision} - projectHref={projectHref} + onPrev={() => navigate(projectHref)} + initialValues={{ + [CURRENCY_AMOUNT]: data?.[CURRENCY_AMOUNT] || 0, + [CREDITS_AMOUNT]: data?.[CREDITS_AMOUNT] || 0, + [SELL_ORDERS]: data?.[SELL_ORDERS] || [], + [CREDIT_VINTAGE_OPTIONS]: data?.[CREDIT_VINTAGE_OPTIONS] || [], + [CURRENCY]: data?.[CURRENCY] || { + askBaseDenom: '', + askDenom: '', + }, + }} /> )} {activeStep === 1 && ( From 681d9f79cc841c9e9fa70faf193b338eaa57d68a Mon Sep 17 00:00:00 2001 From: blushi Date: Mon, 23 Sep 2024 17:45:07 +0200 Subject: [PATCH 08/10] feat: add initialValues to PaymentInfoForm --- .../ChooseCreditsForm/ChooseCreditsForm.tsx | 6 ++-- .../PaymentInfoForm.CardInfo.tsx | 14 ++++++-- .../PaymentInfoForm.CustomerInfo.tsx | 2 +- .../PaymentInfoForm.PaymentInfo.tsx | 14 ++++++-- .../PaymentInfoForm/PaymentInfoForm.schema.ts | 9 +++-- .../PaymentInfoForm/PaymentInfoForm.tsx | 36 +++++++++++++------ .../src/pages/BuyCredits/BuyCredits.Form.tsx | 33 ++++++++++++----- .../src/pages/BuyCredits/BuyCredits.tsx | 2 ++ 8 files changed, 87 insertions(+), 29 deletions(-) diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx index ab44c3fe95..3e45415c1c 100644 --- a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx @@ -1,5 +1,5 @@ -import { Suspense, useCallback, useEffect, useMemo, useState } from 'react'; -import { useFormState, useWatch } from 'react-hook-form'; +import { Suspense, useCallback, useMemo, useState } from 'react'; +import { DefaultValues, useFormState, useWatch } from 'react-hook-form'; import { useLingui } from '@lingui/react'; import { CreditsAmount } from 'web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount'; import { @@ -44,7 +44,7 @@ export type Props = { cardDisabled: boolean; allowedDenoms?: AllowedDenoms; creditTypePrecision?: number | null; - initialValues?: ChooseCreditsFormSchemaType; + initialValues?: DefaultValues; onPrev: () => void; }; diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CardInfo.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CardInfo.tsx index 8e6df93fdf..af635b32ab 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CardInfo.tsx +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.CardInfo.tsx @@ -5,6 +5,7 @@ import { PaymentElement } from '@stripe/react-stripe-js'; import CheckboxLabel from 'web-components/src/components/inputs/new/CheckboxLabel/CheckboxLabel'; import { Body } from 'web-components/src/components/typography'; +import { UseStateSetter } from 'web-components/src/types/react/useState'; import { paymentElementOptions } from './PaymentInfoForm.constants'; import { PaymentInfoFormSchemaType } from './PaymentInfoForm.schema'; @@ -12,8 +13,13 @@ import { PaymentInfoFormSchemaType } from './PaymentInfoForm.schema'; type CardInfoProps = { accountId?: string; className?: string; + setPaymentInfoValid: UseStateSetter; }; -export const CardInfo = ({ accountId, className }: CardInfoProps) => { +export const CardInfo = ({ + accountId, + className, + setPaymentInfoValid, +}: CardInfoProps) => { const ctx = useFormContext(); const { register, control, setValue } = ctx; @@ -32,7 +38,11 @@ export const CardInfo = ({ accountId, className }: CardInfoProps) => { return (
- + setPaymentInfoValid(event.complete)} + /> Input an email address to receive a receipt of your purchase. diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.PaymentInfo.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.PaymentInfo.tsx index fdafc3ba20..ed6fc0c07b 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.PaymentInfo.tsx +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.PaymentInfo.tsx @@ -7,17 +7,20 @@ import { PaymentMethod } from '@stripe/stripe-js'; import Card from 'web-components/src/components/cards/Card'; import { Radio } from 'web-components/src/components/inputs/new/Radio/Radio'; import { Title } from 'web-components/src/components/typography'; +import { UseStateSetter } from 'web-components/src/types/react/useState'; import { CardInfo } from './PaymentInfoForm.CardInfo'; import { PaymentInfoFormSchemaType } from './PaymentInfoForm.schema'; export type PaymentInfoProps = { paymentMethods?: Array | null; + setPaymentInfoValid: UseStateSetter; accountId?: string; }; export const PaymentInfo = ({ paymentMethods, accountId, + setPaymentInfoValid, }: PaymentInfoProps) => { const { _ } = useLingui(); const ctx = useFormContext(); @@ -62,12 +65,19 @@ export const PaymentInfo = ({ {...register(`paymentMethodId`)} > {paymentMethodId === '' && ( - + )} ) : ( - + )} ); diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.schema.ts b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.schema.ts index e388c52569..3baa9eb05b 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.schema.ts +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.schema.ts @@ -1,14 +1,19 @@ import { z } from 'zod'; +import { Wallet } from 'lib/wallet/wallet'; + import { PAYMENT_OPTIONS } from 'pages/BuyCredits/BuyCredits.constants'; import { PaymentOptionsType } from 'pages/BuyCredits/BuyCredits.types'; -export const paymentInfoFormSchema = (paymentOption: PaymentOptionsType) => +export const paymentInfoFormSchema = ( + paymentOption: PaymentOptionsType, + wallet?: Wallet, +) => z.object({ name: paymentOption === PAYMENT_OPTIONS.CARD ? z.string().min(1) : z.string(), email: - paymentOption === PAYMENT_OPTIONS.CARD + paymentOption === PAYMENT_OPTIONS.CARD && !wallet?.address ? z.string().email().min(1) : z.union([z.literal(''), z.string().email().nullable()]), createAccount: z.boolean(), diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx index 65ff43bf7e..9dcec10984 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx @@ -1,5 +1,5 @@ -import { useEffect } from 'react'; -import { useFormState } from 'react-hook-form'; +import { useEffect, useMemo, useState } from 'react'; +import { DefaultValues, useFormState, useWatch } from 'react-hook-form'; import { useLingui } from '@lingui/react'; import { Stripe, StripeElements } from '@stripe/stripe-js'; @@ -29,6 +29,7 @@ export type PaymentInfoFormProps = { setConfirmationTokenId: UseStateSetter; stripe?: Stripe | null; elements?: StripeElements | null; + initialValues?: DefaultValues; } & CustomerInfoProps & PaymentInfoProps; @@ -46,17 +47,19 @@ export const PaymentInfoForm = ({ setConfirmationTokenId, stripe, elements, + initialValues, }: PaymentInfoFormProps) => { const { _ } = useLingui(); const { handleBack } = useMultiStep(); + const [paymentInfoValid, setPaymentInfoValid] = useState(false); const form = useZodForm({ - schema: paymentInfoFormSchema(paymentOption), + schema: paymentInfoFormSchema(paymentOption, wallet), defaultValues: { - email: accountEmail, - name: accountName, - createAccount: true, - savePaymentMethod: true, + email: initialValues?.email || accountEmail, + name: initialValues?.name || accountName, + createAccount: initialValues?.createAccount || true, + savePaymentMethod: initialValues?.savePaymentMethod || true, paymentMethodId: paymentMethods?.[0]?.id, }, mode: 'onBlur', @@ -73,6 +76,14 @@ export const PaymentInfoForm = ({ form.setValue('paymentMethodId', paymentMethods?.[0]?.id); }, [accountEmail, accountName, form, paymentMethods]); + const paymentMethodId = useWatch({ + control: form.control, + name: 'paymentMethodId', + }); + const card = useMemo( + () => paymentOption === PAYMENT_OPTIONS.CARD, + [paymentOption], + ); return (
- {paymentOption === PAYMENT_OPTIONS.CARD && ( - + {card && ( + )}
@@ -128,7 +143,8 @@ export const PaymentInfoForm = ({ saveDisabled={ !isValid || isSubmitting || - (!stripe && paymentOption === PAYMENT_OPTIONS.CARD) + (!stripe && card) || + (card && !paymentMethodId && !paymentInfoValid) } saveText={_(NEXT)} onPrev={handleBack} diff --git a/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx b/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx index 632f9d4258..4eddf2522b 100644 --- a/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx +++ b/web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx @@ -48,6 +48,7 @@ type Props = { retiring: boolean; setRetiring: UseStateSetter; setConfirmationTokenId: UseStateSetter; + setPaymentMethodId: UseStateSetter; cardSellOrders: Array; cryptoSellOrders: Array; creditTypeAbbrev?: string; @@ -59,6 +60,7 @@ export const BuyCreditsForm = ({ retiring, setRetiring, setConfirmationTokenId, + setPaymentMethodId, cardSellOrders, cryptoSellOrders, creditTypeAbbrev, @@ -153,14 +155,11 @@ export const BuyCreditsForm = ({ creditTypePrecision={creditTypeData?.creditType?.precision} onPrev={() => navigate(projectHref)} initialValues={{ - [CURRENCY_AMOUNT]: data?.[CURRENCY_AMOUNT] || 0, - [CREDITS_AMOUNT]: data?.[CREDITS_AMOUNT] || 0, - [SELL_ORDERS]: data?.[SELL_ORDERS] || [], - [CREDIT_VINTAGE_OPTIONS]: data?.[CREDIT_VINTAGE_OPTIONS] || [], - [CURRENCY]: data?.[CURRENCY] || { - askBaseDenom: '', - askDenom: '', - }, + [CURRENCY_AMOUNT]: data?.[CURRENCY_AMOUNT], + [CREDITS_AMOUNT]: data?.[CREDITS_AMOUNT], + [SELL_ORDERS]: data?.[SELL_ORDERS], + [CREDIT_VINTAGE_OPTIONS]: data?.[CREDIT_VINTAGE_OPTIONS], + [CURRENCY]: data?.[CURRENCY], }} /> )} @@ -171,7 +170,11 @@ export const BuyCreditsForm = ({ { - handleSaveNext({ ...data, ...values }); + const { paymentMethodId, ...others } = values; + // we don't store paymentMethodId in local storage for security reasons, + // only in current state + handleSaveNext({ ...others, ...values }); + setPaymentMethodId(paymentMethodId); }} login={onButtonClick} retiring={retiring} @@ -182,6 +185,12 @@ export const BuyCreditsForm = ({ paymentMethods={paymentMethodData?.paymentMethods} setError={setErrorBannerTextAtom} setConfirmationTokenId={setConfirmationTokenId} + initialValues={{ + email: data?.email, + name: data?.name, + createAccount: data?.createAccount, + savePaymentMethod: data?.savePaymentMethod, + }} /> ) : ( @@ -199,6 +208,12 @@ export const BuyCreditsForm = ({ paymentMethods={paymentMethodData?.paymentMethods} setError={setErrorBannerTextAtom} setConfirmationTokenId={setConfirmationTokenId} + initialValues={{ + email: data?.email, + name: data?.name, + createAccount: data?.createAccount, + savePaymentMethod: data?.savePaymentMethod, + }} /> )} diff --git a/web-marketplace/src/pages/BuyCredits/BuyCredits.tsx b/web-marketplace/src/pages/BuyCredits/BuyCredits.tsx index 524995ed0c..4292a9df6c 100644 --- a/web-marketplace/src/pages/BuyCredits/BuyCredits.tsx +++ b/web-marketplace/src/pages/BuyCredits/BuyCredits.tsx @@ -38,6 +38,7 @@ export const BuyCredits = () => { const [confirmationTokenId, setConfirmationTokenId] = useState< string | undefined >(); + const [paymentMethodId, setPaymentMethodId] = useState(); const formModel = getFormModel({ _, paymentOption, retiring }); const sellOrders = useMemo( @@ -76,6 +77,7 @@ export const BuyCredits = () => { onChainProjectId }`} setConfirmationTokenId={setConfirmationTokenId} + setPaymentMethodId={setPaymentMethodId} /> )} From 3348d62b84daab874b3ddeb0b72ec8daf4c9b1ce Mon Sep 17 00:00:00 2001 From: blushi Date: Mon, 23 Sep 2024 17:57:08 +0200 Subject: [PATCH 09/10] test: fix CreditsAmount test --- .../molecules/CreditsAmount/CreditsAmount.stories.tsx | 2 ++ .../molecules/CreditsAmount/CreditsAmount.test.tsx | 1 + .../components/molecules/CreditsAmount/CreditsAmount.tsx | 8 ++------ .../molecules/CreditsAmount/CreditsAmount.types.tsx | 2 +- .../organisms/ChooseCreditsForm/ChooseCreditsForm.tsx | 1 + 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.stories.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.stories.tsx index 740c51b184..75002d11f9 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.stories.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.stories.tsx @@ -30,6 +30,7 @@ const CreditsWithForm = (args: any) => { args.paymentOption === PAYMENT_OPTIONS.CARD ? { askDenom: CURRENCIES.usd, askBaseDenom: CURRENCIES.usd } : defaultCryptoCurrency; + const [currency] = useState(initCurrency); const [spendingCap, setSpendingCap] = useState(0); const [creditsAvailable, setCreditsAvailable] = useState(0); @@ -52,6 +53,7 @@ const CreditsWithForm = (args: any) => { { const formDefaultValues = { paymentOption: PAYMENT_OPTIONS.CARD, + currency: { askDenom: CURRENCIES.usd, askBaseDenom: CURRENCIES.usd }, spendingCap: 3185, setSpendingCap: () => {}, creditsAvailable: 1125, diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx index bf2102c85c..a09a68ed05 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx @@ -28,6 +28,7 @@ import { CreditsInput } from './CreditsInput'; import { CurrencyInput } from './CurrencyInput'; export const CreditsAmount = ({ + currency, paymentOption, creditsAvailable, setCreditsAvailable, @@ -41,12 +42,7 @@ export const CreditsAmount = ({ creditTypePrecision, }: CreditsAmountProps) => { const [maxCreditsSelected, setMaxCreditsSelected] = useState(false); - const { setValue, trigger, control } = - useFormContext(); - const currency = useWatch({ - control, - name: CURRENCY, - }); + const { setValue, trigger } = useFormContext(); const card = paymentOption === PAYMENT_OPTIONS.CARD; const orderedSellOrders = useMemo( diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx index 2ce9b7aa5c..9854ca04ef 100644 --- a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx @@ -5,7 +5,6 @@ import { UseStateSetter } from 'web-components/src/types/react/useState'; import { PaymentOptionsType } from 'pages/BuyCredits/BuyCredits.types'; import { UISellOrderInfo } from 'pages/Projects/AllProjects/AllProjects.types'; -import { ChooseCreditsFormSchemaType } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema'; import { AllowedDenoms } from '../DenomLabel/DenomLabel.utils'; @@ -25,6 +24,7 @@ export interface CreditsAmountProps { cryptoCurrencies: Currency[]; allowedDenoms?: AllowedDenoms; creditTypePrecision?: number | null; + currency: Currency; } export interface CreditsInputProps { diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx index 3e45415c1c..163455c627 100644 --- a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx @@ -203,6 +203,7 @@ export function ChooseCreditsForm({ cryptoCurrencies={cryptoCurrencies} allowedDenoms={allowedDenoms} creditTypePrecision={creditTypePrecision} + currency={currency} /> )} {paymentOption === PAYMENT_OPTIONS.CRYPTO && ( From 8388974a9f961b08cd6ba34a9b4174c81f9b3086 Mon Sep 17 00:00:00 2001 From: blushi Date: Mon, 23 Sep 2024 18:01:50 +0200 Subject: [PATCH 10/10] fix: ts error --- .../components/organisms/PaymentInfoForm/PaymentInfoForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx index 9dcec10984..d25ea5ec38 100644 --- a/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx +++ b/web-marketplace/src/components/organisms/PaymentInfoForm/PaymentInfoForm.tsx @@ -31,7 +31,7 @@ export type PaymentInfoFormProps = { elements?: StripeElements | null; initialValues?: DefaultValues; } & CustomerInfoProps & - PaymentInfoProps; + Omit; export const PaymentInfoForm = ({ paymentOption,