From 708b700fa8fe580a19d1503b7cef561c2a65ca6b Mon Sep 17 00:00:00 2001 From: Alfredo Bonilla Date: Sat, 15 Feb 2025 09:47:08 -0600 Subject: [PATCH] Feature/argent mobile integration b (#122) --- .../app/_components/features/ImageUpload.tsx | 9 +- .../_components/features/ProductCatalog.tsx | 9 +- .../_components/features/WalletConnect.tsx | 87 +++- .../web/src/app/user/register-coffee/page.tsx | 404 +++++++++++------- apps/web/src/providers/starknet.tsx | 9 +- apps/web/src/services/contractsInterface.ts | 6 +- 6 files changed, 335 insertions(+), 189 deletions(-) diff --git a/apps/web/src/app/_components/features/ImageUpload.tsx b/apps/web/src/app/_components/features/ImageUpload.tsx index c4fbe77..4f5af86 100644 --- a/apps/web/src/app/_components/features/ImageUpload.tsx +++ b/apps/web/src/app/_components/features/ImageUpload.tsx @@ -29,7 +29,7 @@ export function ImageUpload({ if (!file) return; try { - // Create preview + // Create temporary preview const objectUrl = URL.createObjectURL(file); setPreviewUrl(objectUrl); setIsUploading(true); @@ -51,7 +51,13 @@ export function ImageUpload({ throw new Error(data.error ?? "Failed to upload image"); } + // Update preview URL to use IPFS gateway + const ipfsUrl = `${process.env.NEXT_PUBLIC_GATEWAY_URL}/ipfs/${data.ipfsHash}`; + setPreviewUrl(ipfsUrl); onImageUploaded(data.ipfsHash); + + // Clean up the temporary object URL + URL.revokeObjectURL(objectUrl); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to upload image"; @@ -88,6 +94,7 @@ export function ImageUpload({ alt="Preview" fill className="object-cover rounded-lg" + unoptimized /> ) : ( diff --git a/apps/web/src/app/_components/features/ProductCatalog.tsx b/apps/web/src/app/_components/features/ProductCatalog.tsx index b5079dc..5027efe 100755 --- a/apps/web/src/app/_components/features/ProductCatalog.tsx +++ b/apps/web/src/app/_components/features/ProductCatalog.tsx @@ -95,6 +95,13 @@ export default function ProductCatalog({ metadata = product.nftMetadata as NftMetadata; } + // Format the image URL to include the IPFS gateway if it's an IPFS hash + const imageUrl = metadata?.imageUrl + ? metadata.imageUrl.startsWith("Qm") + ? `${process.env.NEXT_PUBLIC_GATEWAY_URL}/ipfs/${metadata.imageUrl}` + : metadata.imageUrl + : "/images/cafe1.webp"; + const handleAddToCart = () => { if (!isConnected && onConnect) { onConnect(); @@ -121,7 +128,7 @@ export default function ProductCatalog({ return ( => { if (connector) { - connect({ connector }); + try { + const result = connectWallet({ connector }); + console.log("connector result", result); + } catch (error) { + console.error("Error connecting wallet:", error); + } + } + }; + + const handleConnectArgentMobile = async (): Promise => { + try { + setIsConnecting(true); + const result = await connect({ + webWalletUrl: ARGENT_WEBWALLET_URL, + argentMobileOptions: { + dappName: "CofiBlocks", + url: "https://web.argent.xyz", + }, + }); + + if (result?.connector) { + const connectorResult = connectWallet({ + connector: result.connector as unknown as Connector, + }); + console.log("connectorResult", connectorResult); + } + } catch (error) { + console.error("Error connecting Argent Mobile:", error); + } finally { + setIsConnecting(false); } }; @@ -48,13 +79,16 @@ export default function WalletConnect({ try { const signature = await signTypedDataAsync(); - await signIn("credentials", { + const signInResult = await signIn("credentials", { address, message: JSON.stringify(MESSAGE), redirect: false, signature, }); - onSuccess?.(); + + if (signInResult?.ok) { + onSuccess?.(); + } } catch (err) { console.error(t("error_signing_message"), err); } @@ -82,7 +116,7 @@ export default function WalletConnect({ {address ? ( <> - ))} + + + + {Array.isArray(connectors) && + connectors.map((connector) => ( + + ))} )} diff --git a/apps/web/src/app/user/register-coffee/page.tsx b/apps/web/src/app/user/register-coffee/page.tsx index dac3e6e..540c27c 100644 --- a/apps/web/src/app/user/register-coffee/page.tsx +++ b/apps/web/src/app/user/register-coffee/page.tsx @@ -1,15 +1,15 @@ "use client"; -import { ArrowPathRoundedSquareIcon } from "@heroicons/react/24/outline"; +import { + ArrowPathRoundedSquareIcon, + CameraIcon, +} from "@heroicons/react/24/outline"; +import { ClockIcon } from "@heroicons/react/24/solid"; import { zodResolver } from "@hookform/resolvers/zod"; import Button from "@repo/ui/button"; -import InputField from "@repo/ui/form/inputField"; -import NumericField from "@repo/ui/form/numericField"; import RadioButton from "@repo/ui/form/radioButton"; -import TextAreaField from "@repo/ui/form/textAreaField"; import { useAccount, useProvider } from "@starknet-react/core"; -import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useState } from "react"; +import Image from "next/image"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { z } from "zod"; @@ -24,7 +24,6 @@ import { } from "~/services/contractsInterface"; import { api } from "~/trpc/react"; import { RoastLevel } from "~/types"; -import { getStarknetPrice, usdToStrk } from "~/utils/priceConverter"; const schema = z.object({ roast: z.string().min(1, "Roast level is required"), @@ -37,14 +36,13 @@ const schema = z.object({ .min(0, "Coffee score must be a positive number") .max(100, "Coffee score must be at most 100") .optional(), - image: z.string().min(1, "Image is required"), + image: z.string().optional(), }); type FormData = z.infer; export default function RegisterCoffee() { const { t } = useTranslation(); - const router = useRouter(); const { provider } = useProvider(); const contracts = new ContractsInterface( useAccount(), @@ -54,196 +52,284 @@ export default function RegisterCoffee() { provider, ); const mutation = api.product.createProduct.useMutation(); - const [strkPrice, setStrkPrice] = useState(null); - const [isLoadingPrice, setIsLoadingPrice] = useState(false); - - const fetchStrkPrice = useCallback(async () => { - try { - setIsLoadingPrice(true); - const price = await getStarknetPrice(); - setStrkPrice(price); - } catch (error) { - console.error("Error fetching STRK price:", error); - } finally { - setIsLoadingPrice(false); - } - }, []); - - useEffect(() => { - void fetchStrkPrice(); - // Refresh price every 5 minutes - const interval = setInterval(() => void fetchStrkPrice(), 5 * 60 * 1000); - return () => clearInterval(interval); - }, [fetchStrkPrice]); - const { register, handleSubmit, control, setValue, watch } = + const { register, handleSubmit, control, getValues, setValue } = useForm({ resolver: zodResolver(schema), defaultValues: { roast: RoastLevel.LIGHT, bagsAvailable: 1, - price: "", - coffeeScore: 0, }, }); - const price = watch("price"); - const onSubmit = async (data: FormData) => { - if (!strkPrice) { - alert(t("error_strk_price_unavailable")); - return; - } - - const usdPrice = Number.parseFloat(data.price); - const strkAmount = usdToStrk(usdPrice, strkPrice); - + const submissionData = { + ...data, + price: Number.parseFloat(data.price), + }; + // TODO: Implement coffee registration logic + console.log(submissionData); try { const token_id = await contracts.register_product( - strkAmount, - data.bagsAvailable, + submissionData.price, + submissionData.bagsAvailable, ); - await mutation.mutateAsync({ tokenId: token_id, - name: data.variety, - price: strkAmount, - description: data.description, - image: `${process.env.NEXT_PUBLIC_GATEWAY_URL}/ipfs/${data.image}`, - strength: data.roast, + name: submissionData.variety, + price: submissionData.price, + description: submissionData.description, + image: submissionData.image ?? "/images/cafe1.webp", + strength: submissionData.roast, region: "", farmName: "", }); - - alert(t("product_registered_successfully")); - router.push("/user/my-coffee"); + alert("Product registered successfully"); } catch (error) { if (error instanceof ContractsError) { if (error.code === ContractsError.USER_MISSING_ROLE) { - alert(t("user_not_registered_as_seller")); + alert("User is not registered as a seller"); } else if (error.code === ContractsError.USER_NOT_CONNECTED) { - alert(t("user_disconnected")); + alert("User is disconnected"); } } else { - console.error("Error registering:", error); - alert(t("error_registering_product")); + console.log("error registering", error); + alert("An error occurred while registering the product"); } } }; - const handlePriceChange = (value: string) => { - // Only allow numbers and one decimal point - const formatted = value.replace(/[^\d.]/g, ""); - const parts = formatted.split("."); - if (parts.length > 2) return; // Don't allow multiple decimal points - if (parts[1] && parts[1].length > 2) return; // Only allow 2 decimal places - setValue("price", formatted); - }; - - const getStrkEquivalent = (usdPrice: string): string => { - if (!strkPrice || !usdPrice) return ""; - const usdAmount = Number.parseFloat(usdPrice); - if (Number.isNaN(usdAmount)) return ""; - return `≈ ${usdToStrk(usdAmount, strkPrice).toFixed(2)} STRK`; - }; - return ( - -
-
-
- { - void setValue("image", hash, { - shouldValidate: true, - }); - }} - /> - - - - +
+ +
+
+ { + void setValue("image", hash, { + shouldValidate: true, + }); + }} + /> +
+
+
+ + - - +
+ +