Trustless Work
English
English
  • Welcome
    • Why Escrows Matter
    • Who Should Use This
    • Links
  • ⚒️Core Concepts
    • Smart Escrow Design
      • What does a Smart Escrow "look like"?
    • Roles in Trustless Work
    • Escrow Lifecycle
      • Initiation Phase
      • Funding Phase
      • Complete phase
      • Approval phase
      • Release phase
      • Dispute Resolution
    • USDC: The Stablecoin Powering Trustless Work
    • Stellar & Soroban: The Backbone of Trustless Work
  • 💻Developer Guide
    • 🚀Developer Quickstart
    • Authentication
      • Request Api Key
    • Schema
    • Architecture
    • Stellar Wallets
      • Freighter Wallet
      • Albedo Wallet
      • xBull Wallet
      • Rabet Wallet
      • Lobstr Wallet
      • Hana Wallet
      • Additional Resources
      • Troubleshooting & FAQs
    • How to Get Testnet Tokens
  • API Reference
    • Introduction
    • Types
      • Basic
      • Entities
      • Payloads
      • Responses
      • Errors
    • Single Release Escrow
      • Deploy
      • Fund Escrow
      • Approve Milestone
      • Change Milestone Status
      • Release Funds
      • Dispute Escrow
      • Resolve Dispute
      • Get Escrow
      • Update Escrow
    • Multi Release Escrow
      • Deploy
      • Fund Escrow
      • Approve Milestone
      • Change Milestone Status
      • Release Milestone Funds
      • Dispute Milestone
      • Resolve Milestone Dispute
      • Update Escrow
      • Get Escrow
    • Helpers
      • Set Trustline
      • Send Transaction
      • Get Multiple Escrow Balance
    • 🚀Smart Escrow API
  • React Library
    • Getting Started
    • useSendTransaction
    • useGetEscrow
    • useGetMultipleEscrowBalances
    • useInitializeEscrow
    • useUpdateEscrow
    • useFundEscrow
    • useChangeMilestoneApprovedFlag
    • useChangeMilestoneStatus
    • useReleaseFunds
    • useStartDispute
    • useResolveDispute
  • Templates & Components
    • 🤖Using the dApp
      • Step 1: Accessing the Trustless Work Dapp and Logging In
      • Step 2: Creating a Profile and Requesting an API Key
      • Step 3: Creating an Escrow
      • Step 4: Funding an Escrow
      • Step 5: Marking a Milestone as Done ✅
      • Step 6: Approving the Milestone
      • Step 7: Releasing the Payment
      • Resolving Disputes
    • Integration Demo Project
      • Getting Started
      • Configuration
      • Context API
      • Wallet Setup
      • Trustlines
      • Services
      • Helpers
      • Error Handling
      • Forms
        • Initialize Escrow
        • Fund Escrow
        • Get Escrow
        • Get Multiple Escrow Balances
        • Resolve Dispute
        • Change Milestone Status
        • Change Milestone Flag
        • Change Dispute Flag
        • Release Funds
        • Update Escrow
      • UI Components
        • Pages
        • Cards
        • Tabs
        • Utils
  • Tools & Utilities
  • 🌍Use Cases by Industry
    • Marketplaces & E-commerce
    • Grants, Bounties, and Hackathons
    • P2P Exchanges and OTC Desks
    • Security Deposits
    • Milestone-based Freelance & Contract Work
    • Crowdfunding & Pre-orders
    • DAO Treasury & Working Group Budgets
    • Education & Online Courses
    • Subscription + Performance-based Retainers
  • 🏴‍☠️Community & Contribution
    • 🌎Spotlight: Kindfi
    • 🛣️Roadmap: The Journey Ahead
    • Contributor's Guide
    • 📂Open-Source Templates & Developer Tools
  • 📢Appendix & Archive
    • Contact and Support
    • Notion for Startups
    • Meru
    • ✒️Background & Theory
      • 📘Core Concepts & Escrow Glossary
      • 🎒Historical Context
        • 📜Escrow History:
          • 🗿Ancient to Colonial Era
          • 🏦Banking Era (19th - 20th Century)
          • 💻The Digital Era and Online Escrow
        • ⛓️Blockchain Era: Smart Escrows
Powered by GitBook
LogoLogo

Links

  • Website
On this page
  • Schema
  • Custom Hook
  • Form

Was this helpful?

Export as PDF
  1. Templates & Components
  2. Integration Demo Project
  3. Forms

Change Milestone Status

The ideal schema for this endpoint.

Schema

This validates an escrow form using Zod, including wallet addresses, change status properties and milestone index.

import { isValidWallet } from "@/helpers/valid-data.helper";
import { z } from "zod";

export const formSchema = z.object({
  contractId: z.string().min(1, "Contract ID is required"),
  milestoneIndex: z.string().min(1, "Milestone index is required"),
  newStatus: z.string().min(1, "New status is required"),
  serviceProvider: z
    .string()
    .min(1, {
      message: "Service provider is required.",
    })
    .refine((value) => isValidWallet(value), {
      message: "Service provider must be a valid wallet.",
    }),
  evidence: z.string().optional(),
});

Custom Hook

This contains all the form logic, including schema validation, onSubmit function and others states and functionalities.

import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useEscrowContext } from "@/providers/escrow.provider";
import { formSchema } from "../schemas/change-milestone-status-form.schema";
import { escrowService } from "../services/escrow.service";
import { toast } from "sonner";
import { Escrow, Milestone } from "@/@types/escrows/escrow.entity";
import { EscrowRequestResponse } from "@/@types/escrows/escrow-response.entity";
import { ChangeMilestoneStatusPayload } from "@/@types/escrows/escrow-payload.entity";

export const useChangeMilestoneStatusForm = () => {
  const { escrow } = useEscrowContext();
  const { setEscrow } = useEscrowContext();
  const [loading, setLoading] = useState(false);
  const [response, setResponse] = useState<EscrowRequestResponse | null>(null);

  const milestones = escrow?.milestones || [
    { description: "Initial setup", status: "pending" },
    { description: "Development phase", status: "pending" },
  ];

  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      contractId: escrow?.contractId || "",
      milestoneIndex: "",
      newStatus: "",
      evidence: "",
      serviceProvider: escrow?.roles.serviceProvider || "",
    },
  });

  const onSubmit = async (payload: ChangeMilestoneStatusPayload) => {
    setLoading(true);
    setResponse(null);

    try {
      /**
       * API call by using the escrow service
       * @Note:
       * - We need to specify the endpoint and the method
       * - We need to specify that the returnEscrowDataIsRequired is false
       * - The result will be an EscrowRequestResponse
       */
      const result = (await escrowService.execute({
        payload,
        endpoint: "/escrow/change-milestone-status",
        method: "post",
        returnEscrowDataIsRequired: false,
      })) as EscrowRequestResponse;

      /**
       * @Responses:
       * result.status === "SUCCESS"
       * - Escrow updated successfully
       * - Set the escrow in the context
       * - Show a success toast
       *
       * result.status !== "SUCCESS"
       * - Show an error toast
       */
      if (result.status === "SUCCESS") {
        const escrowUpdated: Escrow = {
          ...escrow!,
          milestones: escrow!.milestones.map((milestone: Milestone, index) =>
            index === Number(payload.milestoneIndex)
              ? {
                  ...milestone,
                  status: payload.newStatus,
                  evidence: payload.evidence || "",
                }
              : milestone
          ),
        };

        setEscrow(escrowUpdated);

        toast.info(
          `Milestone index - ${payload.milestoneIndex} updated to ${payload.newStatus}`
        );
        setResponse(result);
        form.reset();
      }
    } catch (err) {
      toast.error(
        err instanceof Error ? err.message : "An unknown error occurred"
      );
    } finally {
      setLoading(false);
    }
  };

  return { form, milestones, loading, response, onSubmit };
};

Form

This form is built with react hook form. We use the custom hook and zod schema mentioned before.

"use client";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Form,
  FormField,
  FormItem,
  FormLabel,
  FormControl,
  FormMessage,
} from "@/components/ui/form";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { useChangeMilestoneStatusForm } from "../../hooks/change-milestone-status-form.hook";
import { useEscrowContext } from "@/providers/escrow.provider";
import { ResponseDisplay } from "@/components/utils/response-display";

export function ChangeMilestoneStatusForm() {
  const { form, milestones, loading, response, onSubmit } =
    useChangeMilestoneStatusForm();
  const { escrow } = useEscrowContext();

  return (
    <div className="space-y-6">
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
          <FormField
            control={form.control}
            name="contractId"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Contract / Escrow ID</FormLabel>
                <FormControl>
                  <Input
                    placeholder="CAZ6UQX7..."
                    {...field}
                    disabled={!!escrow?.contractId}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />

          <FormField
            control={form.control}
            name="serviceProvider"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Service Provider Address</FormLabel>
                <FormControl>
                  <Input
                    placeholder="GSERVICE..."
                    {...field}
                    disabled={!!escrow?.roles.serviceProvider}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />

          <FormField
            control={form.control}
            name="milestoneIndex"
            render={({ field }) => (
              <FormItem className="w-full">
                <FormLabel>Milestone Index</FormLabel>
                <FormControl>
                  <Select
                    onValueChange={field.onChange}
                    defaultValue={field.value}
                  >
                    <SelectTrigger className="w-full">
                      <SelectValue placeholder="Select a milestone" />
                    </SelectTrigger>
                    <SelectContent>
                      {milestones.map((_, index) => (
                        <SelectItem key={index} value={index.toString()}>
                          Milestone {index + 1}
                        </SelectItem>
                      ))}
                    </SelectContent>
                  </Select>
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />

          <FormField
            control={form.control}
            name="newStatus"
            render={({ field }) => (
              <FormItem>
                <FormLabel>New Status</FormLabel>
                <FormControl>
                  <Input placeholder="Completed" {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />

          <FormField
            control={form.control}
            name="evidence"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Evidence (optional)</FormLabel>
                <FormControl>
                  <Input
                    placeholder="https://example.com/evidence"
                    {...field}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />

          <Button type="submit" className="w-full" disabled={loading}>
            {loading ? "Updating..." : "Change Milestone Status"}
          </Button>
        </form>
      </Form>

      <ResponseDisplay response={response} />
    </div>
  );
}
PreviousResolve DisputeNextChange Milestone Flag

Last updated 20 days ago

Was this helpful?