{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-web3/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":["admonition","tabs","tab"]},"type":"markdown"},"seo":{"title":"Sell Domains with the Partner API","description":"The developer documentation portal and API reference for Unstoppable Domains.","siteUrl":"https://docs.unstoppabledomains.com","keywords":"unstoppable domains developer portal, api reference docs","lang":"en-US","llmstxt":{"hide":false,"sections":[{"title":"Table of contents","includeFiles":["**/*"],"excludeFiles":[]}],"excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"sell-domains-with-the-partner-api","__idx":0},"children":["Sell Domains with the Partner API"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The Partner API v3 provides you with the ability to lookup, register and manage Web3 domains. The API exposes a RESTful interface for interacting with Web3 domains and the Unstoppable Domains registry. Capabilities include:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Lookup Domains: Search for specific domains or find suggested alternatives, to determine pricing, availability and on-chain details"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Registering Domains: Secure domains into your dedicated Custody wallets to maintain the domains on the blockchain"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Manage Domains: Update records on the blockchain or transfer the domain to external owners, all through a simple API interface"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["In this integration guide, you will create a Partner API flow focussing on domain lookup and registration. To complete this integration, you should be a JavaScript developer with experience in RESTful APIs."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If you'd like to skip ahead or follow along, you can clone the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://github.com/unstoppabledomains/demos/tree/vincent/full-flow/Unstoppable%20Partner%20API%20Example"},"children":["full example"]}," from GitHub beforehand."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"step-1-project-setup","__idx":1},"children":["Step 1: Project Setup"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Before you get started, you'll need to install Node >= v18 and npm. Then, download the following setup script in a unix-like environment (MacOS, Linux, WSL, etc) to create the project directory, install the suggested packages, and create the suggested configuration files. If you do not have access to a unix-environment, clone the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://github.com/unstoppabledomains/demos/tree/vincent/full-flow/Unstoppable%20Partner%20API%20Example"},"children":["full example"]}," from GitHub and follow along."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://gist.github.com/V-Shadbolt/ea43bd25d63c1b10fdf8ea1740073290/archive/63f8da87228028c83cad16493ac84de8700de398.zip"},"children":["Download Setup Script"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["After downloading the script, extract and move the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["setup-pav3-guide.sh"]}," file to your desired directory and run the following commands:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"shell","header":{"controls":{"copy":{}}},"source":"chmod +x setup-pav3-guide.sh\n./setup-pav3-guide.sh\n","lang":"shell"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This will create a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["project"]}," folder in your chosen directory that will use throughout this guide."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["@uauth/js"]}," will be the library used for implementing Unstoppable Login on the frontend, ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["axios"]}," will be used for the API calls on both the client and server, ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["nodemon"]}," will be used for easier typescript server development, and ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["lowdb"]}," will act as an interim database on the server to keep the guide self-contained."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"step-2-setup-expressjs","__idx":2},"children":["Step 2: Setup Express.js"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Express.js will serve as your backend throughout this guide. It will handle all interactions with the Partner API, any necessary database operations, and ideally also implement ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://docs.unstoppabledomains.com/domain-distribution-and-management/guides/implementing-webhooks/"},"children":["webhooks"]},". To keep this guide self-contained, you will be utilizing ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["lowdb"]}," as an interim database and will forego webhooks to avoid needing an absolute URL."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["It's very important that the Partner API is not directly accessed from a frontend client as the API key is very sensitive. The API does not handle checkout payments and Unstoppable Domains keeps track of a running balance against the API key for periodic invoicing. It is up to the partner to collect payment from users and subsequently keep their API key secure."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["There is no charge for developing with the Partner API on the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["sandbox"]}," environment. Once you migrate to ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["production"]},", a running balance will be kept against your ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["production API key"]},"."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"environment-variables","__idx":3},"children":["Environment Variables"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Build out your ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./server/.env"]}," file per the below. You can retrieve your Partner API key by following the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://docs.unstoppabledomains.com/domain-distribution-and-management/quickstart/retrieve-an-api-key/"},"children":["Set up Partner API Access Guide"]},"."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"API_URL = 'https://api.ud-sandbox.com/partner/v3'\nAPI_KEY_VALUE = 'xxxxx'\nPORT = 3001\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"express-endpoints","__idx":4},"children":["Express Endpoints"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["With your environment variables configured, you can start outlining the endpoints needed throughout the guide. You will need a way to lookup domain suggestions based on a search query, register domains, and check domain availability.  This means the server will need to be prepared to receive HTTP ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST"]}," requests that have an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["application/json"]}," body as well as general HTTP ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET"]}," requests with url query parameters."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Other considerations:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["As the Partner API is dependant on the blockchain, it provides an operation ID for you to use to check current status. You should have some way of tracking these API operations so you know when the operations complete or if there are any problems and handle them appropriately."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["As there is a running balance against the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["production"]}," API, you should implement a way to know whether the frontend checkout was successful or not, and handle each case. While there is no cost for using ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sandbox"]},", the recommendation is to return the registered domain to Unstoppable should checkout fail for any reason. Returns can be made within 14 days of registration and will be deducted from the running balance."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Should checkout succeed, you should transfer the registered domain to the end-user to custody."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Here is a basic implementation of the three necessary endpoints using Node and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]},". You'll add this to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./server/src/server.ts"]}," file."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import express, { Express, Request, Response } from 'express';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport axios from 'axios';\n\n// Load environment variables from .env file\ndotenv.config();\n\n// Set up the Express application instance\nconst app: Express = express();\nconst port = process.env.PORT || 3001;\n\n// Unstoppable Domains Sandbox API configurations\nconst UNSTOPPABLE_SANDBOX_API_KEY = process.env.API_KEY_VALUE as string;\nconst UNSTOPPABLE_SANDBOX_API_URL = process.env.API_URL as string;\n\n// Middleware setup\napp.use(express.json()); // For parsing JSON request bodies\napp.use(cors()); // Enable Cross-Origin Resource Sharing\n\n/**\n * GET /api/domains - Fetch domain suggestions based on a query string.\n *\n * @query {string} query - The search term for domain suggestions.\n * @returns {Response} - Returns a JSON response with domain suggestions or an error message.\n */\napp.get('/api/domains', async (req: Request, res: Response) => {\n  const query = req.query.query as string;\n  try {\n    const domains = await searchDomains(query);\n    res.json(domains);\n  } catch (error: any) {\n    res.status(500).json({ error: 'Error fetching domains', details: error.message });\n  }\n});\n\n/**\n * POST /api/register - Registers a domain by its ID.\n *\n * @body {string} domainId - The ID of the domain to be registered.\n * @returns {Response} - Returns a JSON response with registration status or an error message.\n */\napp.post('/api/register', async (req: Request, res: Response) => {\n  const domainId = req.body.domainId as string;\n  try {\n    const register = await registerDomain(domainId);\n    if (register.error) {\n      res.status(500).json(register);\n    } else {\n      res.json(register);\n    }\n  } catch (error: any) {\n    res.status(500).json({ error: 'Error registering domain', details: error.message });\n  }\n});\n\n/**\n * POST /api/availability - Checks availability of an array of domains.\n *\n * @body {string[]} domains - Array of domains to check availability.\n * @returns {Response} - Returns a JSON response with availability status or an error message.\n */\napp.post('/api/availability', async (req: Request, res: Response) => {\n  const domains = req.body.domains as string[];\n  try {\n    const availability = await checkAvailability(domains);\n    if (availability.error) {\n      res.status(500).json(availability);\n    } else {\n      res.json(availability);\n    }\n  } catch (error: any) {\n    res.status(500).json({ error: 'Error checking domain availability', details: error.message });\n  }\n});\n\n/**\n * Starts the Express server and listens on the specified port.\n * Logs a message to the console once the server is running.\n */\napp.listen(port, () => {\n  console.log('Server is running on http://localhost:%s', port);\n});\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"partner-api-proxy","__idx":5},"children":["Partner API Proxy"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Now that the Express.js server has appropriate endpoints for domain suggestions, domain registration, and domain availability, you need to proxy these endpoints to the Partner API. You'll use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["searchDomains"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["registerDomain"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["checkAvailability"]}," functions previously defined for this task. Import the appropriate typings from the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./types"]}," directory and make an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["axios"]}," request to the appropriate endpoint:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["searchDomains()"]}," will be proxied to the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://docs.unstoppabledomains.com/apis/partner/#operation/getSuggestions"},"children":["suggestions endpoint"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["registerDomain()"]}," will be proxied to the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://docs.unstoppabledomains.com/apis/partner/#operation/mintSingleDomain"},"children":["registration endpoint"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["checkAvailability()"]}," will be proxied to the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://docs.unstoppabledomains.com/apis/partner/#operation/getMultipleDomains"},"children":["domain details endpoint"]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You'll also add error handling here to encompass any issues with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["axios"]},", or the Partner API. Add these functions to the existing ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["server/src/server.ts"]}," file."]},{"$$mdtype":"Tag","name":"Tabs","attributes":{"size":"medium"},"children":[{"$$mdtype":"Tag","name":"div","attributes":{"label":"searchDomains","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import { Suggestions } from './types/suggestions';\n\n/**\n * Searches for domain suggestions based on the provided domain name.\n *\n * This function makes an API call to the Unstoppable Domains suggestions endpoint\n * to retrieve a list of suggested domains related to the provided 'domainName'.\n * It returns the suggestions data or an error object if the request fails.\n *\n * @param {string} domainName - The domain name query string to search suggestions for.\n * @returns {Promise<Suggestions>} - A promise that resolves to the Suggestions object,\n * containing either the suggestions data or an error object if an error occurs.\n *\n * @throws {Error} - If an error occurs during the API call, this function catches the error\n * and returns an error object with a descriptive message and details about the failure:\n *  - 'Server error' if the server responded with an error\n *  - 'No response received' if there was no response from the server\n *  - 'Error setting up request' if the request could not be configured properly\n */\nconst searchDomains = async (domainName: string): Promise<Suggestions> => {\n  let data = <Suggestions>{};\n  try {\n    const response = await axios.get(\n      UNSTOPPABLE_SANDBOX_API_URL + '/suggestions/domains?query=' + encodeURIComponent(domainName),\n      {\n        headers: {\n          Authorization: 'Bearer ' + UNSTOPPABLE_SANDBOX_API_KEY\n        }\n      }\n    );\n    console.log('Suggestions:', response.data);\n    data = response.data as Suggestions;\n    return data\n  } catch (error: any) {\n    if (error.response) {\n      console.error('Server error:', error.response.data);\n      data.error = { message: 'Server error', details: error.response.data };\n      return data;\n    } else if (error.request) {\n      console.error('No response received:', error.request);\n      data.error = { message: 'No response received', details: error.request };\n      return data;\n    } else {\n      console.error('Error setting up request:', error.message);\n      data.error = { message: 'Error setting up request', details: error.message };\n      return data;\n    }\n  }\n};\n","lang":"typescript"},"children":[]}]},{"$$mdtype":"Tag","name":"div","attributes":{"label":"registerDomain","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import { Order } from './types/orders';\n\n/**\n * Registers a domain with the provided domain ID.\n *\n * This function sends a POST request to the Unstoppable Domains API to register a domain to the default API wallet.\n * On successful registration, it returns the registration details as an 'Order' object.\n * If an error occurs, it returns an error object with relevant details.\n *\n * @param {string} domainId - The ID of the domain to register.\n * @returns {Promise<Order>} - A promise that resolves to the 'Order' object containing the registration details or an error object.\n *\n * @throws {Error} - If an error occurs, it catches the error and returns an error object with:\n *  - 'Server error' if the server responded with an error\n *  - 'No response received' if there was no response from the server\n *  - 'Error setting up request' if the request configuration failed\n */\nconst registerDomain = async (domainId: string): Promise<Order> => {\n  let data = <Order>{};\n  try {\n    const response = await axios.post(\n      UNSTOPPABLE_SANDBOX_API_URL + '/domains?query=' + encodeURIComponent(domainId),\n      JSON.stringify({\n        name: domainId,\n        records: {}\n      }),\n      {\n        headers: {\n          Authorization: 'Bearer ' + UNSTOPPABLE_SANDBOX_API_KEY,\n          'Content-Type': 'application/json'\n        }\n      }\n    );\n    console.log('Domain registered:', response.data);\n    data = response.data as Order;\n    return data\n  } catch (error: any) {\n    if (error.response) {\n      console.error('Server error:', error.response.data);\n      data.error = { message: 'Server error', details: error.response.data };\n      return data;\n    } else if (error.request) {\n      console.error('No response received:', error.request);\n      data.error = { message: 'No response received', details: error.request };\n      return data;\n    } else {\n      console.error('Error setting up request:', error.message);\n      data.error = { message: 'Error setting up request', details: error.message };\n      return data;\n    }\n  }\n};\n","lang":"typescript"},"children":[]}]},{"$$mdtype":"Tag","name":"div","attributes":{"label":"checkAvailability","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import { Domains } from './types/domains';\n\n/**\n * Checks the availability of a list of domains.\n *\n * This function sends a GET request to the Unstoppable Domains API to check the\n * availability of a given list of domain names. It returns the domain details or an\n * error object if an error occurs.\n *\n * @param {Array<string>} domains - The ID of the operation to check.\n * @returns {Promise<Domains>} - A promise that resolves to an 'Operation' object with status details or an error object.\n *\n * @throws {Error} - If an error occurs, it catches the error and returns an error object with:\n *  - 'Server error' if the server responded with an error\n *  - 'No response received' if there was no response from the server\n *  - 'Error setting up request' if the request configuration failed\n */\nconst checkAvailability = async (domains: Array<string>): Promise<Domains> => {\n  let data = <Domains>{};\n  const query = domains.join('&query=');\n  try {\n    const response = await axios.get(\n      UNSTOPPABLE_SANDBOX_API_URL + '/domains?query=' + encodeURIComponent(query),\n      {\n        headers: {\n          Authorization: 'Bearer ' + UNSTOPPABLE_SANDBOX_API_KEY,\n          'Content-Type': 'application/json'\n        }\n      }\n    );\n    console.log('Domain Availability:', response.data);\n    data = response.data as Domains;\n    return data;\n  } catch (error: any) {\n    if (error.response) {\n      console.error('Server error:', error.response.data);\n      data.error = { message: 'Server error', details: error.response.data };\n      return data;\n    } else if (error.request) {\n      console.error('No response received:', error.request);\n      data.error = { message: 'No response received', details: error.request };\n      return data;\n    } else {\n      console.error('Error setting up request:', error.message);\n      data.error = { message: 'Error setting up request', details: error.message };\n      return data;\n    }\n  }\n};\n","lang":"typescript"},"children":[]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You can also take this opportunity to take into account the earlier considerations:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Partner API Operations"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Returns"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Transfers"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Partner API operation tracking will ideally be handled by ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://docs.unstoppabledomains.com/domain-distribution-and-management/guides/implementing-webhooks/"},"children":["webhooks"]}," but, as mentioned, this guide will not encompass public hosting. As such, you'll rely on the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://docs.unstoppabledomains.com/apis/partner/#operation/checkOperation"},"children":["operations endpoint"]},". Similarly, you will use the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://docs.unstoppabledomains.com/apis/partner/#operation/returnDomain"},"children":["returns endpoint"]}," to handle returning domains and will use the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://docs.unstoppabledomains.com/apis/partner/#operation/updateDomainPut"},"children":["overwriting update endpoint"]}," to transfer the domain to the end user."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You would ideally register a webhook for each Partner API operation that is initiated, including a return, registration, transfer, etc. For the purposes of this guide, you can use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["checkOperation()"]}," function as a synchronous polling approach within ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["trackOperation()"]},"."]}]},{"$$mdtype":"Tag","name":"Tabs","attributes":{"size":"medium"},"children":[{"$$mdtype":"Tag","name":"div","attributes":{"label":"checkOperation","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import { Operation } from './types/orders';\n\n/**\n * Checks the status of a domain-related operation.\n *\n * This function sends a GET request to the Unstoppable Domains API to check the\n * status of a given operation by its ID. It returns the operation details or an\n * error object if an error occurs.\n *\n * @param {string} operationId - The ID of the operation to check.\n * @returns {Promise<Operation>} - A promise that resolves to an 'Operation' object with status details or an error object.\n *\n * @throws {Error} - If an error occurs, it catches the error and returns an error object with:\n *  - 'Server error' if the server responded with an error\n *  - 'No response received' if there was no response from the server\n *  - 'Error setting up request' if the request configuration failed\n */\nconst checkOperation = async (operationId: string): Promise<Operation> => {\n  let data = <Operation>{};\n  try {\n    const response = await axios.get(\n      UNSTOPPABLE_SANDBOX_API_URL + '/operations/' + encodeURIComponent(operationId),\n      {\n        headers: {\n          Authorization: 'Bearer ' + UNSTOPPABLE_SANDBOX_API_KEY,\n          'Content-Type': 'application/json'\n        }\n      }\n    );\n    console.log('Operation Status:', response.data);\n    data = response.data as Operation;\n    return data;\n  } catch (error: any) {\n    if (error.response) {\n      console.error('Server error:', error.response.data);\n      data.error = { message: 'Server error', details: error.response.data };\n      return data;\n    } else if (error.request) {\n      console.error('No response received:', error.request);\n      data.error = { message: 'No response received', details: error.request };\n      return data;\n    } else {\n      console.error('Error setting up request:', error.message);\n      data.error = { message: 'Error setting up request', details: error.message };\n      return data;\n    }\n  }\n};\n","lang":"typescript"},"children":[]}]},{"$$mdtype":"Tag","name":"div","attributes":{"label":"trackOperation","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"/**\n * Periodically tracks the status of an operation and updates the database.\n *\n * This function polls the Unstoppable Domains API at a set interval to check the\n * status of a specified operation. It stops tracking if the operation completes.\n *\n * @param {string} operationId - The ID of the operation to track.\n */\nconst trackOperation = async (operationId: string) => {\n  const interval = setInterval(async () => {\n    const operation = await checkOperation(operationId);\n    if (operation.error) {\n      console.log('Error:', operation.error);\n    } else {\n      if (operation.status === 'COMPLETED') {\n        // Handle completed operation\n        clearInterval(interval);\n      }\n      if (operation.status === 'FAILED') {\n        // Handle failed operation\n        clearInterval(interval);\n      }\n      // You would want to ensure you're handling other status cases here\n    }\n  }, 60000); // 1 minute timer\n};\n","lang":"typescript"},"children":[]}]},{"$$mdtype":"Tag","name":"div","attributes":{"label":"returnDomain","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import { Return } from './types/returns';\n\n/**\n * Returns a domain to Unstoppable Domains.\n *\n * This function sends a DELETE request to the Unstoppable Domains API to remove\n * the specified domain from the default API wallet and returns it to Unstoppable Domains. \n * It returns a confirmation or an error object in case of failure. Domains must be returned within 14 days.\n *\n * @param {string} domainId - The ID of the domain to return.\n * @returns {Promise<Return>} - A promise that resolves to a 'Return' object with return details or an error object.\n *\n * @throws {Error} - If an error occurs, it catches the error and returns an error object with:\n *  - 'Server error' if the server responded with an error\n *  - 'No response received' if there was no response from the server\n *  - 'Error setting up request' if the request configuration failed\n */\nconst returnDomain = async (domainId: string): Promise<Return> => {\n  let data = <Return>{};\n  try {\n    const response = await axios.delete(\n      UNSTOPPABLE_SANDBOX_API_URL + '/domains/' + encodeURIComponent(domainId),\n      {\n        headers: {\n          Authorization: 'Bearer ' + UNSTOPPABLE_SANDBOX_API_KEY,\n          'Content-Type': 'application/json'\n        }\n      }\n    );\n    data = response.data as Return;\n    return data\n  } catch (error: any) {\n    if (error.response) {\n      console.error('Server error:', error.response.data);\n      data.error = { message: 'Server error', details: error.response.data };\n      return data;\n    } else if (error.request) {\n      console.error('No response received:', error.request);\n      data.error = { message: 'No response received', details: error.request };\n      return data;\n    } else {\n      console.error('Error setting up request:', error.message);\n      data.error = { message: 'Error setting up request', details: error.message };\n      return data;\n    }\n  }\n};\n","lang":"typescript"},"children":[]}]},{"$$mdtype":"Tag","name":"div","attributes":{"label":"transferDomain","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import { Transfer } from './types/transfers';\n\n/**\n * Transfers a domain to a specified wallet address.\n *\n * This function sends a PUT request to the Unstoppable Domains API to transfer ownership\n * of the specified domain to the provided wallet address. It returns the transfer details\n * or an error object in case of a failure.\n *\n * @param {string} domainId - The ID of the domain to transfer.\n * @param {string} walletAddress - The wallet address to transfer the domain ownership to.\n * @returns {Promise<Transfer>} - A promise that resolves to a 'Transfer' object with transfer details or an error object.\n *\n * @throws {Error} - If an error occurs, it catches the error and returns an error object with:\n *  - 'Server error' if the server responded with an error\n *  - 'No response received' if there was no response from the server\n *  - 'Error setting up request' if the request configuration failed\n */\nconst transferDomain = async (domainId: string, walletAddress: string): Promise<Transfer> => {\n  let data = <Transfer>{};\n  try {\n    const response = await axios.put(\n      UNSTOPPABLE_SANDBOX_API_URL + '/domains/' + encodeURIComponent(domainId),\n      JSON.stringify({\n        name: domainId,\n        owner: {\n          type: 'EXTERNAL',\n          address: walletAddress\n        },\n        records: {}\n      }),\n      {\n        headers: {\n          Authorization: 'Bearer ' + UNSTOPPABLE_SANDBOX_API_KEY,\n          'Content-Type': 'application/json'\n        }\n      }\n    );\n    data = response.data as Transfer;\n    return data\n  } catch (error: any) {\n    if (error.response) {\n      console.error('Server error:', error.response.data);\n      data.error = { message: 'Server error', details: error.response.data };\n      return data;\n    } else if (error.request) {\n      console.error('No response received:', error.request);\n      data.error = { message: 'No response received', details: error.request };\n      return data;\n    } else {\n      console.error('Error setting up request:', error.message);\n      data.error = { message: 'Error setting up request', details: error.message };\n      return data;\n    }\n  }\n};\n","lang":"typescript"},"children":[]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Update the original ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/api/register"]}," endpoint with the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["trackOperation()"]}," function as registrations are blockchain dependant. You do not need to worry about the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transfer"]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["return"]}," functions just yet."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"app.post('/api/register', async (req: Request, res: Response) => {\n  const domainId = req.body.domainId as string;\n  try {\n    const register = await registerDomain(domainId);\n    if (register.error) {\n      res.status(500).json(register);\n    } else {\n      res.json(register);\n      trackOperation(register.operation.id);\n    }\n  } catch (error: any) {\n    res.status(500).json({ error: 'Error registering domain', details: error.message });\n  }\n});\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"mock-database","__idx":6},"children":["Mock Database"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["As the focus of this guide is not databases, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["lowdb"]}," will be used as an interim solution which is a type-safe local JSON database. You'll use these mock databases to store Partner API responses for orders, transfers, and returns in an easily-digestible format. These responses will be used in conjunction with the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["trackOperation()"]}," function to know when you can complete other actions on the domain. Only one operation can be done on a domain at a time so it's important to now when you can act on it again. Add the below to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./server/src/server.ts"]}," file."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To start, specify the storage directory for the JSON files as well as the default data for the JSON. In this case, you can use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./server/src/data/"]}," directory for the JSON files and specify ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["items"]}," as the default data:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import path from 'path';\nimport { dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { JSONFile } from 'lowdb/node';\nimport { Low } from 'lowdb';\nimport { Orders } from './types/orders';\nimport { Transfers } from './types/transfers';\nimport { Returns } from './types/returns';\n\n// Directory setup for the databases\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// Default data for databases\nconst defaultOrderData: Orders = { items: <Order[]>[] }\nconst defaultTransferData: Transfers = { items: <Transfer[]>[] }\nconst defaultReturnData: Returns = { items: <Return[]>[] }\n\n// Paths for local JSON databases\nconst orderDBPath = path.join(__dirname, 'data/orders.json')\nconst transferDBPath = path.join(__dirname, 'data/transfers.json')\nconst returnDBPath = path.join(__dirname, 'data/returns.json')\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Then instantiate and start the databases:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"// LowDB instances for each JSON database\nconst orderDB = new Low(new JSONFile<Orders>(orderDBPath), defaultOrderData);\nconst transferDB = new Low(new JSONFile<Transfers>(transferDBPath), defaultTransferData);\nconst returnDB = new Low(new JSONFile<Returns>(returnDBPath), defaultReturnData);\n\n/**\n * Initializes local JSON databases for orders, transfers, and returns.\n * Reads data from the database files and writes default data if none exists.\n */\nconst initDB = async () => {\n  // Initialize Orders database\n  await orderDB.read();\n  orderDB.data = orderDB.data || defaultOrderData;\n  await orderDB.write();\n\n  // Initialize Transfers database\n  await transferDB.read();\n  transferDB.data = transferDB.data || defaultTransferData;\n  await transferDB.write();\n\n  // Initialize Returns database\n  await returnDB.read();\n  returnDB.data = returnDB.data || defaultReturnData;\n  await returnDB.write();\n  console.log('Databases initialized');\n};\n\n// Initialize the databases on server start\ninitDB().catch((error) => console.error('Error initializing DB:', error));\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Currently, the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["trackOperation()"]}," function only sets an interval for synchronously checking an operation ID until it either succeeds or fails. However, you can utilize this function to keep the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["orders.json"]}," file up-to-date. To do this, you'll need a way to check the operation status currently saved in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["orders"]}," database as well as a way to update it. The below two functions will be able to handle these tasks:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"/**\n * Updates an operation in the database with new operation data.\n *\n * Reads the database, searches for an existing operation by its ID, and updates it\n * if found. Writes the updated data back to the database.\n *\n * @param {Operation} operation - The operation data to update in the database.\n * @param {Low<any>} db - The database instance to perform the update.\n * @returns {Promise<void>} - A promise that resolves when the operation is updated.\n */\nconst updateOperation = async (operation: Operation, db: Low<any>): Promise<void> => {\n  await db.read();\n  const item = db.data.items.find((item: any) => item.operation.id === operation.id);\n  if (item) {\n    item.operation = operation;\n    await db.write();\n  }\n};\n\n/**\n * Retrieves the current status of an operation from the database.\n *\n * This function reads the database, searches for an operation by its ID,\n * and returns its status. If the operation is not found, it returns the provided\n * default status.\n *\n * @param {string} operationId - The ID of the operation to retrieve the status for.\n * @param {string} status - The default status to return if the operation is not found.\n * @param {Low<any>} db - The database instance to search.\n * @returns {Promise<string>} - A promise that resolves to the status of the operation.\n */\nconst getCurrentOperationStatus = async (operationId: string, status: string, db: Low<any>): Promise<string> => {\n  await db.read();\n  const item = db.data.items.find((item: any) => item.operation.id === operationId);\n  if (item) {\n    return item.operation.status;\n  }\n  return status;\n};\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Update the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["trackOperation()"]}," function with database support. Now, it will check if there has been a change in the operation status:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"const trackOperation = async (operationId: string, db: Low<any>) => {\n  const interval = setInterval(async () => {\n    const operation = await checkOperation(operationId);\n    const status = await getCurrentOperationStatus(operationId, operation.status, db);\n    if (operation.error) {\n      console.log('Error:', operation.error);\n    } else {\n      if (operation.status != status) {\n        await updateOperation(operation, db);\n        if (operation.status === 'COMPLETED') {\n          // Handle completed operation\n          clearInterval(interval);\n        }\n        if (operation.status === 'FAILED') {\n          // Handle failed operation\n          clearInterval(interval);\n        }\n        // You would want to ensure you're handling other status cases here\n      }\n    }\n  }, 60000); // 1 minute timer\n};\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["With the databases initialized, update the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/api/register"]}," endpoint with the JSON ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["orderDB"]}," accordingly. This will add the Partner API response for each registration to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./server/src/data/orders.json"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"app.post('/api/register', async (req: Request, res: Response) => {\n  const domainId = req.body.domainId as string;\n  try {\n    const register = await registerDomain(domainId);\n    if (register.error) {\n      res.status(500).json(register);\n    } else {\n      res.json(register);\n      await orderDB.update(({ items }) => items.push(register));\n      trackOperation(register.operation.id, orderDB);\n    }\n  } catch (error: any) {\n    res.status(500).json({ error: 'Error registering domain', details: error.message });\n  }\n});\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["One final consideration with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["lowdb"]}," is that the database stops running when the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]}," server is stopped. As such, it is possible for operations to complete and not be properly tracked. As a workaround, call ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["trackOperation()"]}," when the server starts for any operation in the databases that are neither ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["COMPLETED"]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["FAILED"]},"."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"/**\n * Initializes tracking for any pending operations in the order, transfer, and return databases.\n * Loads the database data and identifies entries where the 'operation.status' is not 'COMPLETED'.\n * For each pending operation, it triggers tracking functions to monitor ongoing processes.\n *\n * @async\n * @function initializeTracking\n * @returns {Promise<void>} - Resolves once all pending operations have been re-tracked.\n */\nconst initializeTracking = async (): Promise<void> => {\n  // Load databases\n  await orderDB.read();\n  await transferDB.read();\n  await returnDB.read();\n\n  // Function to check and track pending operations\n  const checkAndTrackPendingOperations = async (db: Low<any>) => {\n    // Update according to the appropriate status of the operation\n    const pendingItems = db.data?.items?.filter((item: any) => item.operation.status !== 'COMPLETED' && item.operation.status !== 'FAILED') || [];\n    for (const item of pendingItems) {\n      await trackOperation(item.operation.id, db);\n    }\n  };\n\n  // Check and track pending operations in each database\n  await checkAndTrackPendingOperations(orderDB);\n  await checkAndTrackPendingOperations(transferDB);\n  await checkAndTrackPendingOperations(returnDB);\n  console.log('Pending operations re-tracked');\n}\n\n// Call initializeTracking when server starts\ninitializeTracking().catch((error) => console.error('Error initializing tracking:', error));\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"checkout","__idx":7},"children":["Checkout"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["At this point, everything is tied together with the exception of these two unused functions: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["returnDomain()"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transferDomain()"]},". As a reminder, both the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["return"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transfer"]}," functions depend on the status of the frontend checkout. Should checkout succeed, transfer the registered domain to the end user. If checkout fails, return the registered domain to Unstoppable."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["While this will be fully dependent on the frontend solution, keep things simple and leverage the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["lowdb"]}," databases with a set interval. First, you need another endpoint for the frontend to provide checkout updates for each order."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Set up a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST"]}," endpoint on the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]}," server that accepts ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["application/json"]},". You'll need the domain that was purchased, the operation ID of the initial registration, a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["TRUE"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["FALSE"]}," boolean for checkout success, and the wallet address the domain should be transferred to. The below function will use these parameters to update the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["orderDB"]}," with the appropriate data."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"/**\n * POST /api/checkout/:domain - Processes checkout for a domain by updating the order details.\n *\n * @param {string} domain - The domain ID in the URL path.\n * @body {string} wallet - Wallet address for the domain transfer.\n * @body {boolean} payment - Payment confirmation status.\n * @body {string} operationId - Operation ID associated with the checkout.\n * @returns {Response} - Returns a JSON response indicating order processing status.\n */\napp.post('/api/checkout/:domain', async (req: Request, res: Response) => {\n  const domain = req.params.domain as string;\n  const walletAddress = req.body.wallet as string;\n  const payment = req.body.payment as boolean;\n  const operationId = req.body.operationId as string;\n  try {\n    await orderDB.read();\n    const order = orderDB.data.items.find(order => order.operation.id === operationId);\n    if (order) {\n      order.walletAddress = walletAddress;\n      order.payment = payment;\n      await orderDB.write();\n    }\n    res.json('Order for domain ' + domain + ' is being processed');\n  } catch (error: any) {\n    res.status(500).json({ error: 'Error processing checkout', details: error.message });\n  }\n});\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Next, you'll need a function similar to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["trackOperation()"]}," that will track the status of the order checkout against the database. If the registration operation is complete, check if the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["payment"]}," boolean is ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["TRUE"]}," and subsequently check if there is a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["wallet address"]},". Presumably, that will be a successful order. Otherwise, assume failure and return the domain. Depending on the state, call the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["returnDomain()"]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transferDomain"]}," functions and their associated databases."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The below does not account for edge cases and is meant as a starting point."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"/**\n * Monitors the checkout process and handles domain transfer or return based on payment status.\n *\n * This function periodically checks the status of an order associated with the provided domain ID.\n * If the order status is 'COMPLETED' and payment is successful, it transfers the domain to the user's\n * wallet address. If payment is unsuccessful, it returns the domain to Unstoppable Domains.\n *\n * @param {string} operationId - The ID of the operation to monitor during checkout.\n */\nconst trackCheckout = async (operationId: string) => {\n  const interval = setInterval(async () => {\n    await orderDB.read();\n    const order = orderDB.data.items.find(order => order.operation.id === operationId);\n    if (order) {\n      // Successful checkout\n      if (order.operation.status === 'COMPLETED' && order.walletAddress && order.payment === true) {\n        try {\n          const domainTransfer = await transferDomain(order.operation.domain, order.walletAddress);\n          if (domainTransfer.error) {\n            console.log('Error transferring domain:', domainTransfer.error);\n            // Handle failed init transfer\n          } else {\n            console.log('Domain transferred:', domainTransfer);\n            // Handle successful init transfer\n            clearInterval(interval);\n            await transferDB.update(({ items }) => items.push(domainTransfer));\n            trackOperation(domainTransfer.operation.id, transferDB);\n          }\n        } catch (error: any) {\n          console.log('Error transferring domain:', error.message);\n        }\n      // Unsuccessful Checkout\n      } else if (order.operation.status === 'COMPLETED' && order.payment != true) {\n        try {\n          const domainReturn = await returnDomain(order.operation.domain);\n          if (domainReturn.error) {\n            console.log('Error returning domain:', domainReturn.error);\n            // Handle failed init return\n          } else {\n            console.log('Domain returned:', domainReturn);\n            // Handle successful init return\n            clearInterval(interval);\n            await returnDB.update(({ items }) => items.push(domainReturn));\n            trackOperation(domainReturn.operation.id, returnDB);\n          }\n        } catch (error: any) {\n          console.log('Error returning domain:', error.message);\n        }\n      }\n      // You would want to ensure you're handling other status cases here\n    }\n  }, 180000); // 3 minute timer\n};\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Finally, update the original ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/api/register"]}," endpoint with the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["trackCheckout()"]}," function:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"app.post('/api/register', async (req: Request, res: Response) => {\n  const domainId = req.body.domainId as string;\n  try {\n    const register = await registerDomain(domainId);\n    if (register.error) {\n      res.status(500).json(register);\n    } else {\n      res.json(register);\n      await orderDB.update(({ items }) => items.push(register));\n      trackOperation(register.operation.id, orderDB);\n      trackCheckout(register.operation.id);\n    }\n  } catch (error: any) {\n    res.status(500).json({ error: 'Error registering domain', details: error.message });\n  }\n});\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["At this point, you have a completed backend built with Node and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]},"!"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"step-3-setup-nextjs","__idx":8},"children":["Step 3: Setup Next.js"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["With the backend completed, it is now time to focus on the frontend. Next.js will serve this purpose throughout the remainder of this guide. While there are many viable alternatives, Next.js provides easy page and API management."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["In this section of the guide, you will create functions to call the backend, build out an e-commerce cart, checkout and order pages, as well as a general search page. The following sections will not focus on CSS or visual improvements but the initial setup script did include ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Tailwind CSS"]}," and the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://github.com/unstoppabledomains/demos/tree/vincent/full-flow/Unstoppable%20Partner%20API%20Example"},"children":["full example"]}," can be referenced for a CSS outline."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"environment-variables-1","__idx":9},"children":["Environment Variables"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Build out your ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./client/.env"]}," file per the below. You can retrieve your UAuth Client ID key by following the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://docs.unstoppabledomains.com/identity/quickstart/retrieve-client-credentials/"},"children":["Retrieve Client Credentials guide"]},"."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"NEXT_PUBLIC_API_BASE_URL=http://localhost:3001\nNEXT_PUBLIC_CLIENT_ID=1234567890\nNEXT_PUBLIC_REDIRECT_URI=http://localhost:3000\nNEXT_PUBLIC_SCOPES=openid wallet profile\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"expressjs-api","__idx":10},"children":["Express.js API"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["With your environment variables configured, you can start outlining the backend function calls. Per ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Step 2"]},", you have four exposed endpoints on the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]}," server:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST"]}," to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/api/availability"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST"]}," to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/api/register"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET"]}," to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/api/domains"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST"]}," to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/api/checkout/:domain"]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The general outline for each function will be very similar and, with the exception of ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/api/domains"]},", will contain a JSON body. You'll need to call the backend server running on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["port 3001"]}," and handle both the expected result and any possible errors. In the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./client/src/app/api"]}," directory, create the following files and add the outlined example functions."]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fetchAvailability.ts"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["claimDomain.ts"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fetchSuggestions.ts"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["initCheckout.ts"]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["These four functions will serve as the core of your frontend."]},{"$$mdtype":"Tag","name":"Tabs","attributes":{"size":"medium"},"children":[{"$$mdtype":"Tag","name":"div","attributes":{"label":"fetchAvailability.ts","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import axios from 'axios';\nimport { Domains } from '@/types/domains';\n\n/**\n * Checks the availability of a list of domains.\n *\n * @param {string[]} domains - An array of domain names to check for availability.\n * @returns {Promise<Domains>} - A promise that resolves to a 'Domains' object containing availability data for each domain.\n * @throws {Error} - If an error occurs during the request, throws an error with details.\n */\nexport const fetchAvailability = async (domains: string[]) => {\n  try {\n    const url = process.env.NEXT_PUBLIC_API_BASE_URL + '/api/availability';\n    const res = await axios.post(url, \n      {\n        domains: domains,\n      }\n    );\n\n    return res.data as Domains;\n  } catch (err: unknown) {\n    if (err instanceof Error) {\n      throw new Error('Error domain(s) availability: ', err);\n    }\n  }\n}\n","lang":"typescript"},"children":[]}]},{"$$mdtype":"Tag","name":"div","attributes":{"label":"claimDomain.ts","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import axios from 'axios';\nimport { DomainSuggestion } from '../../types/suggestions';\nimport { Order } from '@/types/orders';\n\n/**\n * Attempts to claim a specific domain.\n *\n * @param {DomainSuggestion} selectedDomain - The domain to claim, specified by a 'DomainSuggestion' object.\n * @returns {Promise<Order>} - A promise that resolves to an 'Order' object if the domain is successfully claimed.\n * @throws {Error} - If an error occurs during the request, throws an error with details.\n */\nexport const claimDomain = async (selectedDomain: DomainSuggestion) => {\n  try {\n    const url = process.env.NEXT_PUBLIC_API_BASE_URL + '/api/register';\n    const res = await axios.post(url, \n      {\n        domainId: selectedDomain.name,\n      }\n    );\n\n    return res.data as Order;\n  } catch (err: unknown) {\n    if (err instanceof Error) {\n      throw new Error('Error registering domain(s): ', err);\n    }\n  }\n}\n","lang":"typescript"},"children":[]}]},{"$$mdtype":"Tag","name":"div","attributes":{"label":"fetchSuggestions.ts","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import { Suggestions } from '@/types/suggestions';\nimport axios from 'axios';\n\n/**\n * Fetches domain suggestions based on a search query.\n *\n * @param {string} query - The search term used to find domain suggestions.\n * @returns {Promise<Suggestions>} - A promise that resolves to a 'Suggestions' object containing domain suggestions.\n * @throws {Error} - If an error occurs during the request, throws an error with details.\n */\nexport const fetchSuggestions = async (query: string) => {\n  try {\n    const url = process.env.NEXT_PUBLIC_API_BASE_URL + '/api/domains?query=' + encodeURIComponent(query);\n    const res = await axios.get(url);\n\n    return res.data as Suggestions;\n  } catch (err: unknown) {\n    if (err instanceof Error) {\n      throw new Error('Error fetching domains: ', err);\n    }\n  }\n}\n","lang":"typescript"},"children":[]}]},{"$$mdtype":"Tag","name":"div","attributes":{"label":"initCheckout.ts","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import axios from 'axios';\n\n/**\n * Initializes the checkout process for a specific domain.\n *\n * @param {string} domain - The domain name being checked out.\n * @param {string} walletAddress - The wallet address for the domain transfer.\n * @param {boolean} payment - The payment status; 'true' if payment is confirmed.\n * @param {string} operationId - The unique ID for the checkout operation.\n * @returns {Promise<any>} - A promise that resolves to the server response on checkout initiation.\n * @throws {Error} - If an error occurs during the request, throws an error with details.\n */\nexport const initCheckout = async (domain: string, walletAddress: string, payment: boolean, operationId: string) => {\n  try {\n    const url = process.env.NEXT_PUBLIC_API_BASE_URL + '/api/checkout/' + encodeURIComponent(domain);\n    const res = await axios.post(url, \n      {\n        wallet: walletAddress,\n        payment: payment,\n        operationId: operationId,\n      }\n    );\n\n    return res.data;\n  } catch (err: unknown) {\n    if (err instanceof Error) {\n      throw new Error('Error processing checkout: ', err);\n    }\n  }\n}\n","lang":"typescript"},"children":[]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"search","__idx":11},"children":["Search"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["In the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./client/src/app/page.tsx"]}," file you'll find the default ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Home()"]}," function for a Next.js app. You'll utilize this file for the domain search results."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To start, add the necessary imports at the very top of the file for the required functions and declare the file as a Client Component module with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["use client"]},". If ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["use client"]}," isn't at the very top of your file, you'll run into compilation errors."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"'use client';\nimport React, { useState } from 'react';\nimport { fetchSuggestions } from './api/fetchSuggestions';\nimport { Suggestions } from '../types/suggestions';\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["From there, instantiate the states within the page at the start of the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Home()"]}," function. This includes user input for the domain search, the domain search results, any errors, and pagination information."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"export default function Home() {\n  const [query, setQuery] = useState('');\n  const [domains, setDomains] = useState<Suggestions | null>(null);\n  const [error, setError] = useState('');\n  const [currentPage, setCurrentPage] = useState<number>(1);\n  const [loading, setLoading] = useState(false);\n  const domainsPerPage = 5;\n  return (\n    ...\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Next, you will setup the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["pagination"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["search"]}," functions as well as handle the user input."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["As ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["pagination"]}," will be a standalone function outside of ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Home()"]},", start there. The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["types"]}," needed for the pagination function are not pre-included with the setup script and are provided below. After the closing brace for the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Home()"]}," function, add the following interface definition and related ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Pagination()"]}," function. This function will split the returned list of domain suggestions into equal parts up to a maximum number per page as defined by ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["domainsPerPage"]},". There is some ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Tailwind CSS"]}," included here to make the button usage easier."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"interface PaginationProps {\n  domainsPerPage: number;\n  totalDomains: number;\n  paginate: (pageNumber: number) => void;\n}\n\n/**\n * Pagination component to render page numbers for navigating through domain results.\n *\n * @param {number} domainsPerPage - Number of domains displayed per page.\n * @param {number} totalDomains - Total number of domain results.\n * @param {function} paginate - Callback function to change the page number.\n * @returns {JSX.Element} Pagination buttons for navigation.\n */\nconst Pagination: React.FC<PaginationProps> = ({ domainsPerPage, totalDomains, paginate }) => {\n  const pageNumbers = [];\n\n  for (let i = 1; i <= Math.ceil(totalDomains / domainsPerPage); i++) {\n    pageNumbers.push(i);\n  }\n\n  return (\n    <nav className='flex justify-center m-[20px]'>\n      <ul className='flex list-none gap-[10px]'>\n        {pageNumbers.map(number => (\n          <li key={number}>\n            <button onClick={() => paginate(number)} className='text-white bg-[#007bff] hover:bg-[#0056b3] font-medium px-[10px] py-[5px] rounded-[4px]'>\n              {number}\n            </button>\n          </li>\n        ))}\n      </ul>\n    </nav>\n  );\n};\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["With the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Pagination()"]}," function finished, you will implement the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["search"]}," function and remaining logic. Add the following within the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Home()"]}," function before the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["return"]},". This will be a simple implementation as you'll only need to call the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fetchSuggestions()"]}," function and set the appropriate states like so:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"/**\n * Fetches domain suggestions based on the current search query.\n * Updates the 'domains' state with the response or sets an error message if the fetch fails.\n */\nconst searchDomains = async () => {\n  try {\n    const response = await fetchSuggestions(query);\n    setDomains(response!);\n    setError('');\n    setCurrentPage(1);\n  } catch (error) {\n    console.error(error);\n    setError('Error fetching domains. Please try again.');\n  }\n};\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You'll tie this to the pagination function shortly by doing some preliminary logic:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"// Calculate indexes for pagination based on current page\nconst indexOfLastDomain = currentPage * domainsPerPage;\nconst indexOfFirstDomain = indexOfLastDomain - domainsPerPage;\nconst currentDomains = domains?.items?.slice(indexOfFirstDomain, indexOfLastDomain);\n\n/**\n * Sets the current page for pagination.\n * @param {number} pageNumber - The page number to navigate to.\n */\nconst paginate = (pageNumber: number) => setCurrentPage(pageNumber);\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Next, you need to handle user input. An easy way to handle this will be to leverage HTML forms. However, there are some considerations to make here. Notably, Unstoppable domains have limitations on what constitutes a valid domain name. In this guide, you'll handle this validation within the form submission function but it can be handled at any stage throughout the user input."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"/**\n * Handles form submission for domain search.\n * Validates user input, resets domain state, and initiates domain search.\n * \n * @param {React.FormEvent<HTMLFormElement>} event - The form submit event.\n */\nconst handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n  event.preventDefault();\n  const inputElement = document.getElementById('search') as HTMLInputElement;\n  const inputValue = inputElement.value;\n  \n  // Validation checks\n  const isValidLength = inputValue.length >= 1 && inputValue.length <= 24;\n  const hasValidChars = ![...inputValue].some(char => !/[a-zA-Z0-9-]/.test(char));\n  const startsWithHyphen = inputValue.startsWith('-');\n  const endsWithHyphen = inputValue.endsWith('-');\n  \n  const isValid = isValidLength && hasValidChars && !startsWithHyphen && !endsWithHyphen;\n  \n  if (!isValid) {\n    setDomains(null);\n    setError('Must be 1-24 characters in length, Contain only letters, numbers, or hyphens, and cannot start or end with a hyphen.');\n    return;\n  }\n  setLoading(true);\n  try {\n    setDomains(null); // Clear previous results\n    await searchDomains(); // Fetch new search results\n  } catch (error) {\n    console.error('Error:', error);\n  } finally {\n    setLoading(false); // Reset loading state\n  }\n}\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The last step of the search will be the UI. Again, this guide will not focus on CSS but will provide some to get you started. Add the HTML form to the function return. Remove the existing ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["<div>Hello world!</div>"]},", rename the remaining ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["<main> </main>"]}," tags to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["<div> </div>"]}," tags, and add the below HTML snippets between them."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"html","header":{"controls":{"copy":{}}},"source":"<form className='max-w-md mx-auto min-w-[400px] pt-[40px] pb-[30px]' onSubmit={(e: React.FormEvent<HTMLFormElement>) => {handleSubmit(e)}}>   \n    <div className='relative text-[1.2em] block w-full bg-[#333] rounded-[8px]'>\n        <div className='absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none '>\n            <svg className='w-4 h-4 text-gray-500 dark:text-gray-400' aria-hidden='true' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'>\n                <path stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z'/>\n            </svg>\n        </div>\n        <input type='search' id='search' className='block w-full p-4 ps-10 bg-[#333] placeholder-gray-400 text-white rounded-[8px]' placeholder='Search for your new domain' onChange={(e) => setQuery(e.target.value)} required />\n        <button type='submit' className='text-white absolute end-2.5 bottom-2.5 bg-[#007bff] hover:bg-[#0056b3] font-medium px-4 py-2 rounded-[4px]'>Search</button>\n    </div>\n    <span className='flex text-gray-500 text-center justify-center mt-2'>Must be 1-24 characters in length, Contain only letters, numbers, or hyphens, and cannot start or end with a hyphen.</span>\n</form>\n{error && <div className='text-red-500 text-center mb-[20px]'>{error}</div>}\n","lang":"html"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Below the form, add the list of suggested domains:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"html","header":{"controls":{"copy":{}}},"source":"<div className='flex flex-col items-center'>\n  {loading &&\n    <svg className='animate-spin -ml-1 mr-3 h-5 w-5 text-black' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'>\n      <circle className='opacity-25' cx='12' cy='12' r='10' stroke='currentColor' strokeWidth='4'></circle>\n      <path className='opacity-75' fill='currentColor' d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'></path>\n    </svg>\n  }\n  {currentDomains?.map((domain) => (\n    <div key={domain.name} className='flex justify-between items-center w-full max-w-[600px] p-[10px] m-[10px] bg-[#333] rounded-[4px]'>\n      <div>\n        <p className='text-[1.2em] text-white'>{domain?.name}</p>\n        <p className='text-[#bbb]'>{((domain?.price?.listPrice?.usdCents ?? 0) / 100).toFixed(2)} USD</p>\n      </div>\n      <button className='text-white text-[1.2em] bg-[#007bff] hover:bg-[#0056b3] font-medium px-4 py-2 rounded-[4px]'>Add to Cart</button>\n    </div>\n  ))}\n</div>\n","lang":"html"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Finally, add the pagination buttons below the search results:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"html","header":{"controls":{"copy":{}}},"source":"<Pagination\n  domainsPerPage={domainsPerPage}\n  totalDomains={domains?.items?.length || 0}\n  paginate={paginate}\n/>\n","lang":"html"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You should now have a user-interactable search bar with domain name validation, search results, and search result pagination. Feel free to clean up the default HTML provided by Next.js and to tune the CSS as you see fit!"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"helper-functions","__idx":12},"children":["Helper Functions"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Before proceeding with the rest of the e-commerce experience, you need to implement a nav bar, a helper function, and add create two contexts: one for authentication, and one for the shopping cart."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Start with the helper function first as the contexts rely on it, and the nav bar relies on the contexts. While you utilized ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["lowdb"]}," on the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]}," server to act as a mock database, you're going to utilize the browsers' local storage to handle the data needs on the frontend. There are several caveats with using local storage exclusively for an e-commerce experience but for the purposes of this guide, it will suffice. Create a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["useLocalStorage.ts"]}," file in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./client/src/app/utils"]},"."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import { useCallback, useEffect, useState } from 'react';\n\n/**\n * Custom React hook to manage state with localStorage, syncing updates across browser tabs.\n * Provides a value stored in localStorage and an updater function to modify it.\n *\n * @template T - The type of the state value to be stored.\n * @param {string} storageKey - The localStorage key under which the state is saved.\n * @param {T} fallbackState - The initial value to be used if no item exists in localStorage.\n * @returns {[T, (newValue: T) => void]} - An array containing the current state value and a function to update it.\n */\nfunction useLocalStorage<T>(storageKey: string, fallbackState: T) {\n  const isClient = typeof window !== 'undefined';\n\n  const [value, setValue] = useState<T>(() => {\n    if (isClient) {\n      const storedValue = localStorage.getItem(storageKey);\n      return storedValue ? JSON.parse(storedValue) : fallbackState;\n    }\n    return fallbackState;\n  });\n\n  useEffect(() => {\n    if (isClient) {\n      const storedValue = localStorage.getItem(storageKey);\n      setValue(storedValue ? JSON.parse(storedValue) : fallbackState);\n    }\n  }, [storageKey, isClient]);\n\n  useEffect(() => {\n    const handleChanges = (e: StorageEvent) => {\n      if (e.key === storageKey) {\n        setValue(e.newValue ? JSON.parse(e.newValue) : fallbackState);\n      }\n    }\n    if (isClient) {\n      window.addEventListener('storage', handleChanges);\n    }\n    return () => {\n      if (isClient) {\n        window.removeEventListener('storage', handleChanges);\n      }\n    };\n  }, [storageKey, fallbackState, isClient]);\n\n  const updateStorage = useCallback(\n    (newValue: T) => {\n      setValue(newValue)\n      if (isClient) {\n        localStorage.setItem(storageKey, JSON.stringify(newValue))\n      }\n    },\n    [storageKey, isClient]\n  )\n\n  return [value, updateStorage] as const;\n};\n\nexport default useLocalStorage\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Next, you'll handle the contexts. Contexts are designed to share data across multiple React components such as selected theme, user authentication, preferred language, etc. For the cart context, you'll need functions for:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Adding an item to the cart"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Removing an item from the cart"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Clearing the cart"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Updating the cart items with backend responses"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Create a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["CartContext.tsx"]}," file in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./client/src/app/context"]}," and add the following:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"'use client';\nimport { DomainSuggestion } from '@/types/suggestions';\nimport { createContext, useContext, ReactNode } from 'react';\nimport useLocalStorage from '../utils/useLocalStorage';\nimport { CartItem } from '@/types/cart';\n\n/**\n * @typedef {Object} CartContextType - Defines the context type for the cart.\n * @property {CartItem[]} cart - Array of items in the cart.\n * @property {(item: DomainSuggestion) => void} addToCart - Function to add a domain suggestion to the cart.\n * @property {(name: string) => void} removeFromCart - Function to remove a domain by name from the cart.\n * @property {(name: string, operationId: string) => void} updateCartItemOperation - Updates the operation ID of a cart item.\n * @property {(name: string, availability: boolean) => void} updateCartItemAvailability - Updates availability status of a cart item.\n * @property {() => void} clearCart - Clears all items from the cart.\n */\n\ninterface CartContextType {\n  cart: CartItem[];\n  addToCart: (item: DomainSuggestion) => void;\n  removeFromCart: (name: string) => void;\n  updateCartItemOperation: (name: string, operationId: string) => void;\n  updateCartItemAvailability: (name: string, availability: boolean) => void;\n  clearCart: () => void;\n}\n\n/** Context to manage cart state throughout the application */\nconst CartContext = createContext<CartContextType | undefined>(undefined);\n\n/**\n * CartProvider component to wrap children and provide cart context.\n *\n * @param {Object} props - Props passed to the provider component.\n * @param {ReactNode} props.children - The components that will consume cart context.\n * @returns {JSX.Element} Context provider with cart functionalities.\n */\nexport const CartProvider = ({ children }: { children: ReactNode }) => {\n  const [cart, setCart] = useLocalStorage<CartItem[]>('CART_STORAGE', []);\n\n  /**\n   * Adds a new item to the cart if it doesn't already exist.\n   * @param {DomainSuggestion} item - The domain suggestion to add.\n   */\n  const addToCart = (item: DomainSuggestion) => {\n    const newItem = { suggestion: item, available: true, operationId: '' };\n    const newCart = cart.some(cartItem => cartItem.suggestion.name === newItem.suggestion.name)\n    ? cart\n    : [...cart, newItem];\n    setCart(newCart);\n  };\n\n  /**\n   * Removes an item from the cart by its domain name.\n   * @param {string} name - The name of the domain to remove.\n   */\n  const removeFromCart = (name: string) => {\n    setCart(cart.filter((item: CartItem) => item.suggestion.name !== name));\n  };\n\n  /**\n   * Updates the operation ID for a specific cart item.\n   * @param {string} name - Name of the cart item to update.\n   * @param {string} operationId - The new operation ID to set.\n   */\n  const updateCartItemOperation = (name: string, operationId: string) => {\n    const updatedCart = cart.map((item) => \n      item.suggestion.name === name ? { ...item, operationId } : item\n    );\n    setCart(updatedCart);\n  };\n\n  /**\n   * Updates the availability status of a specific cart item.\n   * @param {string} name - Name of the cart item to update.\n   * @param {boolean} available - Availability status to set.\n   */\n  const updateCartItemAvailability = (name: string, available: boolean) => {\n    const updatedCart = cart.map((item) => \n      item.suggestion.name === name ? { ...item, available } : item\n    );\n    setCart(updatedCart);\n  };\n\n  /** Clears all items from the cart. */\n  const clearCart = () => setCart([]);\n\n  return (\n    <CartContext.Provider value={{ cart, addToCart, removeFromCart, updateCartItemOperation, updateCartItemAvailability, clearCart }}>\n      {children}\n    </CartContext.Provider>\n  );\n};\n\n/**\n * Custom hook to use the CartContext.\n * Throws an error if used outside of CartProvider.\n * @returns {CartContextType} The cart context value.\n */\nexport const useCart = (): CartContextType => {\n  const context = useContext(CartContext);\n  if (!context) {\n    throw new Error('useCart must be used within a CartProvider');\n  }\n  return context;\n};\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Repeat the process above for the auth context. This guide uses Unstoppable Login for the auth provider and will need two functions:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Login"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Logout"]}]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You can safely ignore the typescript error on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["@uauth/js"]}," in regards to a missing declaration file."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Create a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["AuthContext.tsx"]}," file in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./client/src/app/context"]}," and add the following:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"'use client';\nimport { createContext, useContext, ReactNode, useState } from 'react';\nimport useLocalStorage from '../utils/useLocalStorage';\nimport UAuth from '@uauth/js';\nimport { Authorization } from '@/types/auth';\n\n/**\n * @typedef {Object} AuthContextType - Defines the context type for authentication.\n * @property {Authorization | null} auth - The current authentication details.\n * @property {boolean} authorizing - Indicates if authentication is in progress.\n * @property {() => void} login - Initiates the login process.\n * @property {() => void} logout - Logs the user out.\n */\n\ninterface AuthContextType {\n  auth: Authorization | null;\n  authorizing: boolean;\n  login: () => void;\n  logout: () => void;\n}\n\n/** Context to manage authentication state throughout the application */\nconst AuthContext = createContext<AuthContextType | undefined>(undefined);\n\n// UAuth instance for managing user authentication\nconst uauth = new UAuth({\n    clientID: process.env.NEXT_PUBLIC_CLIENT_ID,\n    redirectUri: process.env.NEXT_PUBLIC_REDIRECT_URI,\n    scopes: process.env.NEXT_PUBLIC_SCOPES\n  });\n\n/**\n * AuthProvider component to wrap children and provide authentication context.\n *\n * @param {Object} props - Props passed to the provider component.\n * @param {ReactNode} props.children - The components that will consume auth context.\n * @returns {JSX.Element} Context provider with authentication functionalities.\n */\nexport const AuthProvider = ({ children }: { children: ReactNode }) => {\n  const [auth, setAuth] = useLocalStorage<Authorization | null>('AUTH_STORAGE', null);\n  const [authorizing, setAuthorizing] = useState(false);\n\n  /**\n   * Initiates the login process with Unstoppable Domains.\n   * Sets auth state on successful verification.\n   */\n  const login = async () => {\n    try {\n        setAuthorizing(true);\n        const authorization = await uauth.loginWithPopup();\n        setAuth(authorization || null);\n    } catch (error) {\n        setAuth(null);\n        console.log('Error logging in: ' + error);\n    } finally {\n      setAuthorizing(false);\n    }\n  };\n\n  /**\n   * Logs the user out and clears the auth state.\n   */\n  const logout = async() => {\n    await uauth.logout();\n    setAuth(null)\n  };\n\n  return (\n    <AuthContext.Provider value={{ auth, authorizing, login, logout }}>\n      {children}\n    </AuthContext.Provider>\n  );\n};\n\n/**\n * Custom hook to use the AuthContext.\n * Throws an error if used outside of AuthProvider.\n * @returns {AuthContextType} The auth context value.\n */\nexport const useAuth = (): AuthContextType => {\n  const context = useContext(AuthContext);\n  if (!context) {\n    throw new Error('useAuth must be used within an AuthProvider');\n  }\n  return context;\n};\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Finally, add these contexts to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["layout.tsx"]}," file in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./client/src/app"]}," like so:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import { CartProvider } from './context/CartContext';\nimport { AuthProvider } from './context/AuthContext';\n...\nreturn (\n  <html lang='en'>\n    <body className={'antialiased'}>\n      <AuthProvider>\n        <CartProvider>\n          {children}\n        </CartProvider>\n      </AuthProvider>\n    </body>\n  </html>\n);\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For the Navbar, you'll need a way for users to login with Unstoppable and to access their cart. As this is mainly CSS, this guide will jump ahead to the implementation. Add the following to your ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["NavBar.tsx"]}," file in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./client/src/app/components"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import Link from 'next/link';\nimport { useCart } from '../context/CartContext';\nimport { useAuth } from '../context/AuthContext';\nimport { useEffect, useState } from 'react';\n\n/**\n * Nav component that renders the application header with links to the cart and account information.\n * Displays a loading spinner during client-side hydration.\n *\n * @returns {JSX.Element} The header and navigation elements of the application.\n */\nconst Nav = () => {\n  const { cart } = useCart();\n  const { auth, authorizing, login, logout } = useAuth();\n  const [isClient, setIsClient] = useState(false); // Tracks client-side rendering status\n\n  /**\n   * Initiates the login process for wallet connection.\n   */\n  const connectWallet = () => {\n    try {\n      login();\n    } catch (error) {\n      console.error('Error:', error);\n    }\n  };\n\n  /**\n   * Initiates the logout process to disconnect the wallet.\n   */\n  const disconnectWallet = () => {\n    try {\n      logout();\n    } catch (error) {\n      console.error('Error:', error);\n    }\n  };\n\n  useEffect(() => {\n    setIsClient(true); // Set to true once the component has mounted client-side\n  }, []);\n\n  // If rendering server-side, display loading state to avoid flash of un-hydrated content.\n  if (!isClient) {\n    return (\n      <header className='bg-[#007bff] p-[20px] text-white text-[2em] text-center rounded-[4px] flex justify-between items-center'>\n        <h1>\n          <Link href='/'>Unstoppable Domains Partner API Example</Link>\n        </h1>\n        <nav className='flex flex-row space-x-4 text-lg'>\n        <div className='h-5 w-5 m-auto'>\n          <svg className='animate-spin -ml-1 mr-3 h-5 w-5 text-white' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'>\n            <circle className='opacity-25' cx='12' cy='12' r='10' stroke='currentColor' strokeWidth='4'></circle>\n            <path className='opacity-75' fill='currentColor' d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'></path>\n          </svg>\n          </div>\n      </nav>\n      </header>\n    );\n  }\n\n  return (\n    <header className='bg-[#007bff] p-[20px] text-white text-[2em] text-center rounded-[4px] flex justify-between items-center'>\n      <h1>\n        <Link href='/'>Unstoppable Domains Partner API Example</Link>\n      </h1>\n      <nav className='flex flex-row space-x-4 text-lg'>\n        <Link href='/cart' className='flex flex-row m-auto h-10 w-150'>\n          <div className='h-5 w-5 m-auto'>\n            <svg className=' items-center justify-center' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>\n              <path d='M3 3H5L5.4 5M7 13H17L21 5H5.4M7 13L5.4 5M7 13L4.70711 15.2929C4.07714 15.9229 4.52331 17 5.41421 17H17M17 17C15.8954 17 15 17.8954 15 19C15 20.1046 15.8954 21 17 21C18.1046 21 19 20.1046 19 19C19 17.8954 18.1046 17 17 17ZM9 19C9 20.1046 8.10457 21 7 21C5.89543 21 5 20.1046 5 19C5 17.8954 5.89543 17 7 17C8.10457 17 9 17.8954 9 19Z' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round'/>\n            </svg>\n          </div>\n          <div className='h-auto m-auto pl-1 '>\n            <span>Cart ({cart.length})</span>\n          </div>\n        </Link>\n        {auth ? \n          <button type='button' onClick={() => disconnectWallet()} className='flex flex-row m-auto h-10 w-150'>\n            <div className='h-5 w-5 m-auto'>\n              <svg className='items-center justify-center' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>\n                <path d='M7 17v1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1a3 3 0 0 0-3-3h-4a3 3 0 0 0-3 3Zm8-9a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round'/>\n              </svg>     \n            </div>\n            <div className='h-auto m-auto pl-1'>\n              <span>{auth.idToken.sub}</span>\n            </div>\n          </button>\n        : <button type='button' onClick={() => connectWallet()} className='flex flex-row m-auto h-10 w-150'>\n            \n            <div className='h-5 w-5 m-auto'>\n              {authorizing ?\n                <svg className='animate-spin -ml-1 mr-3 h-5 w-5 text-white' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'>\n                  <circle className='opacity-25' cx='12' cy='12' r='10' stroke='currentColor' strokeWidth='4'></circle>\n                  <path className='opacity-75' fill='currentColor' d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'></path>\n                </svg>\n              : <svg className='items-center justify-center' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>\n                  <path d='M7 17v1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1a3 3 0 0 0-3-3h-4a3 3 0 0 0-3 3Zm8-9a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round'/>\n                </svg>\n              }\n            </div>\n            <div className='h-auto m-auto pl-1'>\n              <span>Account</span>\n            </div>\n          </button>\n        }\n      </nav>\n    </header>\n  );\n};\n\nexport default Nav;\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Finally, add the Navbar and cart context to the search HTML in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./client/src/app/page.tsx"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import Nav from './components/NavBar';\nimport { useCart } from './context/CartContext';\n...\nconst [currentPage, setCurrentPage] = useState<number>(1);\nconst [loading, setLoading] = useState(false);\nconst { cart, addToCart, removeFromCart } = useCart();\nconst domainsPerPage = 5;\n...\nreturn (\n<div>\n  <Nav />\n  ...\n  <div className='flex flex-col items-center'>\n    ...\n    {currentDomains?.map((domain) => (\n      <div key={domain?.name} className='flex justify-between items-center w-full max-w-[600px] p-[10px] m-[10px] bg-[#333] rounded-[4px]'>\n        <div>\n          <p className='text-[1.2em] text-white'>{domain?.name}</p>\n          <p className='text-[#bbb]'>{((domain?.price?.listPrice?.usdCents ?? 0) / 100).toFixed(2)} USD</p>\n        </div>\n        {cart.some(cartItem => cartItem?.suggestion?.name === domain?.name)\n          ? <button onClick={() => removeFromCart(domain?.name)} className='text-[#49a668] text-[1.2em] bg-[#edf7f4] font-medium px-4 py-2 rounded-[4px]'>Added</button>\n          : <button onClick={() => addToCart(domain)} className='text-white text-[1.2em] bg-[#007bff] hover:bg-[#0056b3] font-medium px-4 py-2 rounded-[4px]'>Add to Cart</button>\n        }\n      </div>\n    ))}\n  </div>\n  ...\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["At this stage, you have a completed homepage that includes a search bar for Unstoppable domains, a way for users to add and remove those items from their shopping cart, and a way for user to login to the site. You now need to build the cart and checkout flow."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"cart","__idx":13},"children":["Cart"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You'll need to add a dedicated ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/cart"]}," route on the frontend to act as the e-commerce shopping cart. To start, create a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["page.tsx"]}," file in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./client/src/app/cart"]}," directory. Below are the necessary imports as well as the component outline: calculate the total dollar value of the cart in USD, define a function for Unstoppable login, and add a rough return function."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"'use client';\nimport Link from 'next/link';\nimport Nav from '../components/NavBar';\nimport { claimDomain } from '../api/claimDomain';\nimport { useEffect, useState } from 'react';\nimport { useRouter } from 'next/navigation';\nimport { useCart } from '../context/CartContext';\nimport { useAuth } from '../context/AuthContext';\nimport { fetchAvailability } from '../api/fetchAvailability';\n\nconst Cart = () => {\n  const { cart, removeFromCart, updateCartItemOperation, updateCartItemAvailability, clearCart } = useCart();\n  const { auth, login } = useAuth();\n  const [error, setError] = useState('');\n  const [loading, setLoading] = useState(false);\n  const router = useRouter();\n  const [isClient, setIsClient] = useState(false);\n  const [allAvailable, setAllAvailable] = useState(false);\n  const [availabilityLoading, setAvailabilityLoading] = useState(false);\n\n  // Calculate total price of items in the cart\n  let total = 0;\n  cart.forEach((item) => {\n    total += (item?.suggestion?.price?.listPrice?.usdCents ?? 0);\n  });\n\n  /**\n   * Initiates the login process for wallet connection.\n   */\n  const connectWallet = () => {\n    try {\n      login();\n    } catch (error) {\n      console.error('Error:', error);\n    }\n  }\n\n  // Early return to avoid server-side rendering issues\n  if (!isClient) {\n    return (\n      <section>\n        <Nav />\n      </section>\n    );\n  }\n\n  return (\n    <section>\n      <Nav />\n      <div>\n      </div>\n    </section>\n  )\n};\n\nexport default Cart;\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Cart()"]}," function will be one of the larger components you'll need to build out. The reason for this is this function will be responsible for both checking the availability of the domains added to the cart as well as registering those domains once checkout is initiated."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You need to check availability because it is possible that user b will purchase the domain(s) in user a's cart. In a similar vein, as soon as user a proceeds from the cart to checkout, you'll register the domains to your API key. This ensures the domains are not available for any other user to claim; avoiding issues of user a purchasing a domain that is no longer available. As you're already handling returns on the backend, you won't need to worry about the user not completing the checkout."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For availability checks, you can periodically check domains in the cart are available with a simple interval, and you'll need to check immediately before registering the domains. Below is a one minute interval that starts when the user accesses the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/cart"]}," route as well as the necessary availability function that will use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fetchAvailability()"]}," endpoint."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"/**\n * Sets client-side flag and periodically checks domain availability in the cart every minute.\n */\nuseEffect(() => {\n  setIsClient(true);\n  // Reset cart operationId on load\n  cart.forEach((item) => {\n    updateCartItemOperation(item.suggestion.name, '');\n  });\n  // Check if all cart items are available on load\n  setAllAvailable(cart.every(item => item.available ?? false));\n  // Periodic check every 60 seconds for domain availability in the cart\n  const interval = setInterval(() => {\n    setAvailabilityLoading(true);\n    checkCartAvailability(); // Check the cart availability on interval\n    setAvailabilityLoading(false);\n  }, 60000);\n\n  return () => clearInterval(interval); // Cleanup on component unmount\n}, []);\n\n/**\n * Checks the availability of all domains in the cart.\n * It updates the cart items' availability status based on the API response.\n *\n * @returns {Promise<boolean>} - Returns a boolean indicating if all items in the cart are available.\n */\nconst checkCartAvailability = async (): Promise<boolean> => {\n  try {\n    setError('');\n    const domains: string[] = cart.map(item => item.suggestion.name); // Collect domain names from the cart\n    if (domains.length > 0) {\n      interface Status {\n        name: string;\n        available: boolean;\n      }\n      const statuses: Status[] = [];\n      // Call external availability check function\n      const availability = await fetchAvailability(domains);\n      // Update each cart item’s availability based on the API response\n      if (availability?.items) {\n        for (const item of availability?.items) {\n          const cartItem = cart.find((cartItem) => cartItem.suggestion.name === item.name);\n          if (cartItem) {\n            if (item.availability.status === 'AVAILABLE') {\n              updateCartItemAvailability(item.name, true)\n              statuses.push({ name: item.name, available: true })\n            } else {\n              updateCartItemAvailability(item.name, false)\n              statuses.push({ name: item.name, available: false })\n            }\n          }\n        }\n      }\n      // Handle error when item availability is missing\n      // Check if all cart items are available\n      setAllAvailable(cart.every(item => item.available ?? false));\n      // Return true if all items are available, otherwise false\n      return statuses.every(item => item.available ?? false);\n    }\n    return false; // Return false if there are no items in the cart\n  } catch (error) {\n    console.log('Error checking domain availability:', error);\n    setError('An unexpected error occurred. Please try again.');\n    return false;\n  }\n};\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Similarly, create the domain registration function that uses the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["claimDomain()"]}," endpoint. As mentioned, you'll check domain availability first before attempting to register the domain to the API key."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"/**\n * Registers the domains in the cart by first checking their availability and then attempting to claim each domain.\n * If any domain is unavailable or an error occurs during the registration, an error message is displayed.\n *\n * @returns {Promise<boolean>} - Returns a boolean indicating if the domain registration was successful.\n */\nconst registerDomain = async (): Promise<boolean> => {\n  try {\n      setError('');\n      setAvailabilityLoading(true);\n      const available = await checkCartAvailability(); // Check availability of all domains in the cart\n      setAvailabilityLoading(false);\n      // Display error message if any domain is unavailable\n      if (!available) {\n        setError('One or more items in your cart are no longer available. Please remove them before proceeding.');\n        return false;\n      }\n      // Attempt to claim each domain in the cart\n      for (const item of cart) {\n        try {\n          const claim = await claimDomain(item.suggestion); // Attempt to claim the domain\n          if (claim?.operation?.id) {\n            updateCartItemOperation(item.suggestion.name, claim?.operation.id); // Update operation ID for the item based on claim response\n          }\n          // Handle any errors when ID is missing\n        } catch (error) {\n          console.log('Error registering ' + item.suggestion.name + ':', error);\n          setError('An unexpected error occurred while claiming ' + item.suggestion.name + '.');\n          return false;\n        }\n      };\n      return true; // Return true if all domains are successfully claimed\n  } catch (error) {\n    console.log('Error registering domains:', error);\n    setError('An unexpected error occurred. Please try again.');\n    return false;\n  }\n};\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Then, much like the search bar on the homepage, leverage HTML forms for user interactivity and implement a function to handle ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["HTMLFormElement"]}," events:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"/**\n * Handles the form submission for checkout. It triggers the checkout process\n * and navigates to the checkout page upon success.\n * \n * @param {React.FormEvent<HTMLFormElement>} event - The form submission event.\n */\nconst handleCheckout = async (event: React.FormEvent<HTMLFormElement>) => {\n  event.preventDefault();\n  setLoading(true);\n  let success = false;\n  try {\n    success = await registerDomain();\n  } catch (error) {\n    console.log('Error:', error);\n  } finally {\n    setLoading(false);\n    if (success) {\n      router.push('/checkout');\n    }\n  }\n}\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You now have all of the functions you need added to the page and can turn your focus to the HTML and CSS. Below is a rough starting point that you're encouraged to fine-tune to your liking! In general, you need a list of all the domain in the cart, show a total value, and provide typical UI for clearing the cart and removing individual items. To callback to the backend ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]}," server, you also need the users' wallet address to transfer the domain to upon successful purchase, add that to you HTML."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"return (\n  <section>\n    <Nav />\n    <div className='mx-auto max-w-screen-xl px-4 2xl:px-0 pt-5'>\n      <div className='mt-6 sm:mt-8 md:gap-6 lg:flex lg:items-start xl:gap-8'>\n        {cart.length === 0 ? (\n          <p className='text-center text-lg text-gray-500 dark:text-gray-400 mt-10 mx-auto'>\n            Your cart is empty.\n          </p>\n        ) : (\n          <div>\n            {cart.map((item) => (\n              <div key={item?.suggestion?.name} className='pb-5'>\n                <div>\n                  <div className='rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800 md:p-6'>\n                    <div className='space-y-4 md:flex md:items-center md:justify-between md:gap-6 md:space-y-0'>\n                      <svg className='h-20 w-20' focusable='false' aria-hidden='true' viewBox='0 0 40 40'>\n                        <path d='M38.3333 3.90803V16.5517L1.66666 31.4942L38.3333 3.90803Z' fill='#00C9FF'></path><path d='M31.4583 3.33333V25.1724C31.4583 31.5203 26.3281 36.6667 20 36.6667C13.6719 36.6667 8.54166 31.5203 8.54166 25.1724V15.977L15.4167 12.1839V25.1724C15.4167 26.2394 15.8392 27.2626 16.5913 28.0171C17.3434 28.7716 18.3635 29.1954 19.4271 29.1954C20.4907 29.1954 21.5108 28.7716 22.2629 28.0171C23.015 27.2626 23.4375 26.2394 23.4375 25.1724V7.75862L31.4583 3.33333Z' fill='#0D67FE'></path>\n                      </svg>\n                      <div className='flex items-center justify-between md:order-3 md:justify-end'>\n                        <div className='text-end md:order-4 md:w-32'>\n                          <p className='text-base font-bold text-gray-900 dark:text-white'>{((item?.suggestion?.price?.listPrice?.usdCents ?? 0) / 100).toFixed(2)} USD</p>\n                        </div>\n                      </div>\n    \n                      <div className='flex flex-col w-full min-w-0 flex-1 space-y-4 md:order-2 md:max-w-md'>\n                        <span className='text-base font-medium text-gray-900 dark:text-white'>{item?.suggestion?.name}</span>\n                        {availabilityLoading &&\n                          <svg className='animate-spin -ml-1 mr-3 h-5 w-5 text-white' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'>\n                            <circle className='opacity-25' cx='12' cy='12' r='10' stroke='currentColor' strokeWidth='4'></circle>\n                            <path className='opacity-75' fill='currentColor' d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'></path>\n                          </svg>\n                        }\n                        {!availabilityLoading && !item?.available && <span className='text-xs font-medium text-red-600 dark:text-red-500'>Domain is no longer available</span>}\n                        {!availabilityLoading && item?.available && <span className='text-xs font-medium text-green-600 dark:text-green-500'>Domain is available</span>}\n                        <div className='flex items-center gap-4'>\n                          <button type='button' className='inline-flex items-center text-sm font-medium text-red-600 hover:underline dark:text-red-500' onClick={() => removeFromCart(item?.suggestion?.name)}>\n                            <svg className='me-1.5 h-5 w-5' aria-hidden='true' xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'>\n                              <path stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M6 18 17.94 6M18 18 6.06 6' />\n                            </svg>\n                            Remove\n                          </button>\n                        </div>\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            ))}\n            <Link href='/cart' onClick={() => clearCart()} className='flex flex-row font-medium text-gray-500 max-w-[120px] max-h-[20px]'>\n              <div className='h-auto'>\n                <span>Clear Cart</span>\n              </div>\n            </Link>\n          </div>\n        )}\n        {cart.length > 0 &&\n          <div className='mx-auto mt-6 max-w-4xl flex-1 space-y-6 lg:mt-0 lg:w-[25%]'>\n            <div className='space-y-4 rounded-[8px] border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6'>\n              <p className='text-xl font-semibold text-gray-900 dark:text-white'>Order summary</p>\n\n              <div className='space-y-4'>\n                <dl className='flex items-center justify-between gap-4 border-t border-gray-200 pt-2 dark:border-gray-700'>\n                  <dt className='text-base font-bold text-gray-900 dark:text-white'>Total</dt>\n                  <dd className='text-base font-bold text-gray-900 dark:text-white'>{((total ?? 0) / 100).toFixed(2)} USD</dd>\n                </dl>\n              </div>\n\n              <form onSubmit={handleCheckout}>\n              { (auth && allAvailable) ? \n                <button type='submit' className='flex mx-auto w-[50%] md:w-[40%] items-center justify-center rounded-lg bg-[#007bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800'>\n                  {loading &&\n                    <svg className='animate-spin -ml-1 mr-3 h-5 w-5 text-white' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'>\n                      <circle className='opacity-25' cx='12' cy='12' r='10' stroke='currentColor' strokeWidth='4'></circle>\n                      <path className='opacity-75' fill='currentColor' d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'></path>\n                    </svg>\n                  }\n                  Proceed to Checkout\n                </button>\n              : <div className='flex mx-auto w-[50%] md:w-[40%] items-center cursor-not-allowed justify-center rounded-lg bg-[#007bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800'>\n                  Proceed to Checkout\n                </div>\n              }\n              </form>\n              {error && <div className='text-red-500 text-center mb-[20px]'>{error}</div>}\n              <div className='flex items-center justify-center gap-2'>\n                <span className='text-sm font-normal text-gray-500 dark:text-gray-400'> or </span>\n                <Link href='/' className='inline-flex items-center gap-2 text-sm font-medium underline hover:no-underline text-[#007bff]'>\n                  Continue Shopping\n                </Link>\n              </div>\n              { auth ? \n                <p className='text-sm font-normal text-gray-500 dark:text-gray-400 text-center'>\n                  Connected Wallet Address:&nbsp;\n                  <span className='items-center gap-2 text-sm font-medium text-[#007bff]'>\n                    {auth?.idToken?.sub}&nbsp;\n                    <span className='text-xs text-gray-500 dark:text-gray-400'>\n                      ({auth?.idToken?.wallet_address})\n                    </span> \n                  </span>\n                </p>\n              : <p className='text-sm font-normal text-gray-500 dark:text-gray-400 text-center'>\n                  One or more items in your cart require a wallet connection.&nbsp;\n                  <button onClick={() => connectWallet()} title='' className='font-medium text-primary-700 underline hover:no-underline dark:text-primary-500'>\n                    Connect your wallet now.\n                  </button>\n                </p>\n              }\n            </div>\n          </div>\n        }\n      </div>\n    </div>\n  </section>\n);\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You've now completed building out the cart! You'll be able to search for domains on the homepage, add those domains to the cart, and view the new cart page."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"checkout-1","__idx":14},"children":["Checkout"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You'll need to add a dedicated ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/checkout"]}," route on the frontend to act as your payment gateway. As a reminder, the Partner API does not handle payments. It is up to the partner to integrate a payment gateway of their choice and to charge the end user in whatever fiat / crypto they so choose. Note that Unstoppable Domains will invoice the partner based on the API price."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To start, create a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["page.tsx"]}," file in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./client/src/app/checkout"]}," directory. Import the required packages and create the component outline: add minimal logic to redirect the user away from the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/checkout"]}," page if they shouldn't be there, calculate total cart value, and add the HTML return."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Partners can use any payment gateway and collect payment in any fiat / crypto they prefer. Partners are also free to set their own pricing however Unstoppable will invoice based on the API returned cost of the domain."]}]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"'use client';\nimport { useCart } from '../context/CartContext';\nimport Nav from '../components/NavBar';\nimport { useEffect, useState } from 'react';\nimport { useRouter } from 'next/navigation';\nimport { useAuth } from '../context/AuthContext';\nimport { initCheckout } from '../api/initCheckout';\nimport Link from 'next/link';\nimport useLocalStorage from '../utils/useLocalStorage';\n\n/**\n * Checkout component manages the checkout process, including the countdown timer, domain transfer,\n * and final checkout submission. It checks if the cart has items and if the user is authenticated \n * before proceeding. The user has a two-minute window to complete the checkout process.\n * \n * @component\n */\nconst Checkout = () => {\n  const { cart } = useCart();\n  const { auth } = useAuth();\n  const [error, setError] = useState('');\n  const [loading, setLoading] = useState(false);\n  const router = useRouter();\n  const [isClient, setIsClient] = useState(false);\n  const [expired, setExpired] = useState(false);\n  const [timeLeft, setTimeLeft] = useState<number>(0);\n  const [startTime, setStartTime] = useLocalStorage<number | null>('CHECKOUT_TIME', null);\n  const countdownTime = 120;\n\n  /**\n   * Redirects the user to the cart page if the cart is empty, invalid, or the user is not authenticated.\n   */\n  useEffect(() => {\n    if (cart.length === 0 || !auth) {\n      router.push('/cart');\n    } else if (cart.some(item => item.operationId === '')) {\n      router.push('/cart');\n    }\n  }, [cart, auth, router]);\n\n  /**\n   * Ensures the component is only rendered on the client side.\n   */\n  useEffect(() => {\n    setIsClient(true);\n  }, []);\n\n  // Calculate total price of items in the cart\n  let total = 0;\n  cart.forEach((item) => {\n    total += (item?.suggestion?.price?.listPrice?.usdCents ?? 0);\n  });\n\n  // Early return to avoid server-side rendering issues\n  if (!isClient) {\n    return (\n      <section>\n        <Nav />\n      </section>\n    );\n  }\n\n  return (\n    <section>\n      <Nav />\n      <div>\n      </div>\n    </section>\n  )\n};\n\nexport default Checkout;\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Checkout()"]}," function will be responsible for using the last ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["initCheckout()"]}," endpoint defined earlier. It's important to note that this guide will not encompass integration of a payment gateway. Instead, you will leverage HTML forms and no data will be stored."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["On the backend, there was an assumed three (3) minute timer for the checkout flow on whether payment has succeeded or failed. So, you will start by implementing a similar timer on the frontend. Ideally, this will be a shorter time-frame than the backend expects to allow for some buffer. You'll get the current time, calculate the remaining time, and format the time from seconds back into a user-digestible format. Keep in mind that official payment gateways may already have a timeout that would make this function redundant."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"/**\n * Calculates the remaining time on the countdown timer.\n */\nuseEffect(() => {\n  const currentTime = Math.floor(Date.now() / 1000); // Get current timestamp in seconds\n  if (startTime) {\n    const elapsedTime = currentTime - startTime;\n    const remainingTime = countdownTime - elapsedTime;\n    // If time expired, stop the checkout process\n    if (remainingTime <= 0) {\n      setExpired(true);\n      setTimeLeft(0);\n      setStartTime(null);\n      return;\n    }\n    // Otherwise, update the time left\n    setTimeLeft(remainingTime);\n  } else {\n    // If no start time, initialize the countdown\n    setStartTime(currentTime);\n    setTimeLeft(countdownTime);\n    setExpired(false);\n  }\n  /**\n   * Interval to update the countdown timer every second.\n   */\n  const interval = setInterval(() => {\n    setTimeLeft((prevTime) => {\n      if (prevTime <= 0) {\n        clearInterval(interval);\n        setExpired(true);\n        setStartTime(null);\n        return 0;\n      }\n      return prevTime - 1;\n    });\n  }, 1000);\n  // Cleanup the interval on component unmount or when countdown is finished\n  return () => clearInterval(interval);\n}, []);\n\n/**\n * Helper function to format remaining time into minutes and seconds.\n * @param {number} seconds - The time in seconds to format.\n * @returns {string} - Formatted time string in 'minutes:seconds' format.\n */\nconst formatTime = (seconds: number): string => {\n  const minutes = Math.floor(seconds / 60);\n  const secs = seconds % 60;\n  return minutes + ':' + (secs < 10 ? '0' : '') + secs;\n};\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Now you can create the checkout function that uses the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["initCheckout()"]}," endpoint. Ensure you have the users' wallet address for the domain transfer as well as the registration operation ID and handle any errors appropriately."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"/**\n * Executes the checkout process for each item in the cart.\n * It tries to process each domain in the cart and queues the transfer using initCheckout.\n * If any error occurs during the transfer, an error message is set and the process stops.\n * \n * @returns {Promise<boolean>} - Returns a boolean indicating if the checkout was successful.\n */\nconst checkout = async (): Promise<boolean> => {\n  try {\n    setError('');\n    for (const item of cart) {\n      try {\n          if (auth?.idToken.wallet_address && item.operationId) {\n            await initCheckout(item.suggestion.name, auth?.idToken.wallet_address, true, item.operationId);\n          }\n          // Handle any errors when wallet_address and operation ID are missing\n        } catch (error) {\n          console.log('Error registering ' + item.suggestion.name + ':', error);\n          setError('An unexpected error occurred while claiming ' + item.suggestion.name + '.');\n          return false; // If an error occurs for a domain, return false to halt checkout\n        }\n    };\n    return true; // Return true if all domains are successfully processed\n  } catch (error) {\n    console.error('Error processing domains:', error);\n    setError('An unexpected error occurred. Please try again.');\n    return false; // Return false if there's an issue with the overall checkout\n  }\n};\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Then, much like the search bar on the homepage, leverage HTML forms for user interactivity and implement a function to handle ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["HTMLFormElement"]}," events:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"/**\n * Handles the form submission for checkout. It triggers the checkout process\n * and navigates to the order page upon success.\n * \n * @param {React.FormEvent<HTMLFormElement>} event - The form submission event.\n */\nconst handleCheckout = async (event: React.FormEvent<HTMLFormElement>) => {\n  event.preventDefault();\n  setLoading(true); // Set loading state to true during checkout process\n  let success = false;\n  try {\n    success = await checkout(); // Attempt to process checkout\n  } catch (error) {\n    console.error('Error:', error);\n  } finally {\n    setLoading(false); // Reset loading state after checkout process\n    if (success) {\n      router.push('/order'); // Navigate to order page upon successful checkout\n    }\n  }\n}\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You now have all of the functions needed added to the page and can turn your focus to the HTML and CSS. Below is a rough starting point that you're encouraged to fine-tune to your liking! In general, you need to display the form for the chosen payment gateway as well as the total the user will be charged. You should also display the wallet address the domain will be transferred to along with the checkout timer."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"return (\n  <section>\n    <Nav />\n    <div className='mx-auto max-w-screen-xl px-4 2xl:px-0 pt-5'>\n      <div>\n        <div className='mt-6 sm:mt-8 lg:flex lg:items-start lg:gap-12'>\n        <form onSubmit={handleCheckout} action='/order' className='w-full rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6 lg:max-w-xl lg:p-8'>\n            <div className='mb-6 grid grid-cols-2 gap-4'>\n            <div className='col-span-2 sm:col-span-1'>\n                <label htmlFor='full_name' className='mb-2 block text-sm font-medium text-gray-900 dark:text-white'> Full name (as displayed on card)* </label>\n                <input type='text' id='full_name' className='block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder:text-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500' placeholder='Partner Engineering' required />\n            </div>\n\n            <div className='col-span-2 sm:col-span-1'>\n                <label htmlFor='card-number-input' className='mb-2 block text-sm font-medium text-gray-900 dark:text-white'> Card number* </label>\n                <input type='text' id='card-number-input' className='block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 pe-10 text-sm text-gray-900 focus:border-primary-500 focus:ring-primary-500  dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder:text-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500' placeholder='4242424242424242' required />\n            </div>\n\n            <div>\n                <label htmlFor='card-expiration-input' className='mb-2 flex items-center gap-1 text-sm font-medium text-gray-900 dark:text-white'>Card expiration* </label>\n                <input id='card-expiration-input' type='text' className='block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder:text-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500' placeholder='1234' required />\n            </div>\n\n            <div>\n                <label htmlFor='cvv-input' className='mb-2 flex items-center gap-1 text-sm font-medium text-gray-900 dark:text-white'>CVV*</label>\n                <input type='number' id='cvv-input' className='block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder:text-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500' placeholder='567' required />\n            </div>\n            </div>\n\n            <button type='submit' className='flex w-full items-center justify-center rounded-lg bg-[#007bff] px-5 py-2.5 text-sm font-medium text-white focus:outline-none focus:ring-4' disabled={expired}>\n            {loading &&\n                <svg className='animate-spin -ml-1 mr-3 h-5 w-5 text-white' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'>\n                <circle className='opacity-25' cx='12' cy='12' r='10' stroke='currentColor' strokeWidth='4'></circle>\n                <path className='opacity-75' fill='currentColor' d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'></path>\n                </svg>\n            }\n            {expired ? 'Checkout expired' : 'Pay now'}\n            </button>\n            {error && <div className='text-red-500 text-center mb-[20px]'>{error}</div>}\n            {expired ?\n            <div className='flex items-center justify-center gap-2 mt-6'>\n                <Link href='/cart' className='inline-flex items-center gap-2 text-sm font-medium underline hover:no-underline text-[#007bff]'>\n                Return to Cart\n                <svg className='h-5 w-5' aria-hidden='true' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'>\n                    <path stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M19 12H5m14 0-4 4m4-4-4-4' />\n                </svg>\n                </Link>\n            </div>\n            : \n            <div className='mt-6 mx-auto text-center text-gray-500 dark:text-gray-400'>Checkout time remaining: {formatTime(timeLeft)}</div>\n            }\n        </form>\n\n        <div className='mt-6 grow sm:mt-8 lg:mt-0'>\n            <div className='space-y-4'>\n            <dl className='flex items-center justify-between gap-4 border-t border-gray-200 pt-2 dark:border-gray-700'>\n                <dt className='text-base font-bold text-gray-400'>Total</dt>\n                <dd className='text-base font-bold text-gray-400'>{((total ?? 0) / 100).toFixed(2)} USD</dd>\n            </dl>\n            </div>\n        </div>\n        </div>\n        { auth && \n        <p className='mt-3 text-sm font-normal text-gray-500 dark:text-gray-400 text-center mx-auto lg:text-left'>\n            Domain will transfer after checkout to Wallet Address:&nbsp;\n            <span className='gap-2 text-sm font-medium text-[#007bff]'>\n            {auth?.idToken?.sub}&nbsp;\n            <span className='text-xs text-gray-500 dark:text-gray-400'>\n                ({auth?.idToken?.wallet_address})\n            </span> \n            </span>\n        </p>\n        }\n      </div>\n    </div>\n  </section>\n);\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"orders","__idx":15},"children":["Orders"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Finally, you can create the order, or confirmation, page. Here you'll simply outline the details of the domain registration and transfer. This will be mainly CSS and HTML but you'll need to ensure you empty the cart context as appropriate and redirect the user if they shouldn't be on the page yet. To start, create a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["page.tsx"]}," file in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./client/src/app/order"]}," directory."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"'use client';\nimport { useCart } from '../context/CartContext';\nimport Link from 'next/link';\nimport Nav from '../components/NavBar';\nimport { useAuth } from '../context/AuthContext';\nimport { useRouter } from 'next/navigation';\nimport { useEffect, useState } from 'react';\n\n/**\n * Order confirmation page component.\n * \n * This component displays an order summary with a thank-you message. \n * If the user is not authenticated or has an empty cart, it redirects them to the cart page.\n * \n * @component\n */\nconst Order = () => {\n  const { cart, clearCart } = useCart();\n  const { auth } = useAuth();\n  const router = useRouter();\n  const [isClient, setIsClient] = useState(false);\n\n  /**\n   * Redirects the user to the cart page if the cart is empty, invalid, or the user is not authenticated.\n   */\n  useEffect(() => {\n    if (cart.length === 0 || !auth) {\n      router.push('/cart');\n    } else if (cart.some(item => item.operationId === '')) {\n      router.push('/cart');\n    }\n  }, [cart, auth, router]);\n\n  /**\n   * Ensures the component is only rendered on the client side.\n   */\n  useEffect(() => {\n    setIsClient(true);\n  }, []);\n\n  // Early return to avoid server-side rendering issues\n  if (!isClient) {\n    return (\n      <section>\n        <Nav />\n      </section>\n    );\n  }\n\n  return (\n    <section>\n      <Nav />\n      <div>\n      </div>\n    </section>\n  );\n};\n\nexport default Order;\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Then you can add the below HTML to the return:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"return (\n  <section>\n    <Nav />\n    <div className='mx-auto max-w-screen-xl px-4 2xl:px-0 pt-5'>\n      <h2 className='mt-6 text-xl font-semibold text-gray-400 sm:text-2xl mb-2'>Thanks for your order!</h2>\n      <p className='text-gray-500 dark:text-gray-400 mb-6 md:mb-8'>Your order <a href='#' className='font-medium text-gray-400 hover:underline'>#{Math.floor(100000 + Math.random() * 900000)}</a> will be processed within a few minutes. Keep an eye on your wallet for the domain.</p>\n      <div className='w-[75%] space-y-4 sm:space-y-2 rounded-lg border border-gray-100 bg-gray-50 p-6 dark:border-gray-700 dark:bg-gray-800 mb-6 md:mb-8'>\n        <dl className='sm:flex items-center justify-between gap-4'>\n          <dt className='font-normal mb-1 sm:mb-0 text-gray-500 dark:text-gray-400'>Date</dt>\n          <dd className='font-medium text-gray-900 dark:text-white sm:text-end'>{new Date().toLocaleString()}</dd>\n        </dl>\n        <dl className='sm:flex items-center justify-between gap-4'>\n          <dt className='font-normal mb-1 sm:mb-0 text-gray-500 dark:text-gray-400'>Payment Method</dt>\n          <dd className='font-medium text-gray-900 dark:text-white sm:text-end'>Credit Card</dd>\n        </dl>\n        <dl className='sm:flex items-center justify-between gap-4'>\n          <dt className='font-normal mb-1 sm:mb-0 text-gray-500 dark:text-gray-400'>Minting Wallet</dt>\n          <dd className='font-medium text-gray-900 dark:text-white sm:text-end'>{auth?.idToken?.sub}</dd>\n        </dl>\n      </div>\n      <div className='flex items-center space-x-4'>\n        <Link href='/' onClick={() => clearCart()} className='flex flex-row gap-2 items-center justify-center rounded-lg bg-[#007bff] px-5 py-2.5 text-sm font-medium text-white focus:outline-none focus:ring-4'>\n          Return to shopping\n        </Link>\n      </div>\n    </div>\n  </section>\n);\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["That's it! You are now running a local e-commerce platform for selling Unstoppable domains."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"running-the-project","__idx":16},"children":["Running the Project"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["From the root project directory ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["./project"]},", run the following command:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"shell","header":{"controls":{"copy":{}}},"source":"npm run start\n","lang":"shell"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This will concurrently start both the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]}," backend and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Next.js"]}," frontend. If you want to run just one or the other, you can use either of the below commands:"]},{"$$mdtype":"Tag","name":"Tabs","attributes":{"size":"medium"},"children":[{"$$mdtype":"Tag","name":"div","attributes":{"label":"Frontend","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"shell","header":{"controls":{"copy":{}}},"source":"npm run start:client\n","lang":"shell"},"children":[]}]},{"$$mdtype":"Tag","name":"div","attributes":{"label":"Backend","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"shell","header":{"controls":{"copy":{}}},"source":"npm run start:server\n","lang":"shell"},"children":[]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"recap","__idx":17},"children":["Recap"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["As a recap, you implemented an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Express.js"]}," backend that supports the following Partner API functions:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Domain Suggestion lookup"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Domain Availability lookup"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Domain Registration"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Domain Transfers"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Domain Returns"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Partner API Operation Tracking"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Then, you built out a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Next.js"]}," frontend that implements four exposed ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]}," endpoints for ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["search"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["availability"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["registration"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["checkout"]}," to allow users to:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Search for available Unstoppable domains"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Add Unstoppable domains to a shopping cart"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Add their payment details to a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fake"]}," payment gateway"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Receive their Unstoppable domain(s)"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Continue to build upon this demo and happy coding!"]}]},"headings":[{"value":"Sell Domains with the Partner API","id":"sell-domains-with-the-partner-api","depth":1},{"value":"Step 1: Project Setup","id":"step-1-project-setup","depth":2},{"value":"Step 2: Setup Express.js","id":"step-2-setup-expressjs","depth":2},{"value":"Environment Variables","id":"environment-variables","depth":3},{"value":"Express Endpoints","id":"express-endpoints","depth":3},{"value":"Partner API Proxy","id":"partner-api-proxy","depth":3},{"value":"Mock Database","id":"mock-database","depth":3},{"value":"Checkout","id":"checkout","depth":3},{"value":"Step 3: Setup Next.js","id":"step-3-setup-nextjs","depth":2},{"value":"Environment Variables","id":"environment-variables-1","depth":3},{"value":"Express.js API","id":"expressjs-api","depth":3},{"value":"Search","id":"search","depth":3},{"value":"Helper Functions","id":"helper-functions","depth":3},{"value":"Cart","id":"cart","depth":3},{"value":"Checkout","id":"checkout-1","depth":3},{"value":"Orders","id":"orders","depth":3},{"value":"Running the Project","id":"running-the-project","depth":2},{"value":"Recap","id":"recap","depth":2}],"frontmatter":{"title":"Selling Domains with the Partner API | Unstoppable Domains Developer Portal","description":"This page details basic configuration and usage of the Partner API.","seo":{"title":"Sell Domains with the Partner API"}},"editPage":{"to":"https://github.com/unstoppabledomains/dev-docs/blob/main/web3/domain-distribution-and-management/guides/sell-domains.md"},"lastModified":"2026-04-10T16:45:57.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/web3/domain-distribution-and-management/guides/sell-domains","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}