diff --git a/app/components/BuyConfigBox.tsx b/app/components/BuyConfigBox.tsx new file mode 100644 index 0000000..e7d2326 --- /dev/null +++ b/app/components/BuyConfigBox.tsx @@ -0,0 +1,33 @@ +import { Button, Card, Link, Select, SelectItem } from "@nextui-org/react"; +import { useState } from "react"; +import ReactJson from "react-json-view"; +import { generateBuyConfig } from "../utils/queries"; +import { scrollToHeader } from "../utils/helpers"; + + +export function BuyConfigBox() { + // json response containing BuyConfig and BuyOptions + const [buyConfig, setBuyConfig] = useState(); + + const buyConfigurationWrapper = async () => { + const response = await generateBuyConfig(); + try { + setBuyConfig(response); + } catch (error) { + alert(error); + } + } + + return ( + +

scrollToHeader("buyConfigHeader")}> Generate Buy Config:

+

The Buy Config API returns the list of countries supported by Coinbase Onramp, and the payment methods available in each country.

+
+ + + {buyConfig && } + +
+
+ ); +} \ No newline at end of file diff --git a/app/components/BuyQuoteBox.tsx b/app/components/BuyQuoteBox.tsx new file mode 100644 index 0000000..a4a7329 --- /dev/null +++ b/app/components/BuyQuoteBox.tsx @@ -0,0 +1,336 @@ +import { Button } from "@nextui-org/button"; +import { Input } from "@nextui-org/input"; +import { ChangeEvent, useCallback, useMemo, useRef, useState } from "react"; +import { Divider } from "@nextui-org/divider"; +import { Card, Link, Tooltip } from "@nextui-org/react"; +import {Select, SelectItem} from "@nextui-org/select"; +import { BuyOptionsRequest, BuyOptionsResponse, BuyQuoteRequest, BuyQuoteResponse} from "../utils/types"; +import { generateBuyOptions, generateBuyQuote } from "../utils/queries"; +import ReactJson from "react-json-view"; +import { BuyConfigBox } from "./BuyConfigBox"; +import { scrollToHeader } from "../utils/helpers"; +import SecureTokenBox from "./SecureTokenBox"; + +export default function BuyQuoteBox() { + + // Buy Options API Request Parameters & wrapper function to change parameter state + const [buyOptionsParams, setBuyOptionsParams] = useState({ + country: '', + subdivision: '', + }) + const onChangeBuyOptionsParams = (e: ChangeEvent) => { + const { name, value } = e.target; + setBuyOptionsParams(prevState => ({ + ...prevState, + [name]: value + })); + } + + // Buy Options API Response + const [buyOptionsResponse, setBuyOptionsResponse] = useState(); + const prevCountrySubdiv = useRef(''); + + const emptyBuyQuoteParams = { + purchase_currency: '', + payment_currency: '', + payment_method: '', + country: '', + payment_amount: '', + purchase_network: '', + } + + // Buy Quote Request Parameters & wrapper function to change parameter state + const [buyQuoteParams, setBuyQuoteParams] = useState(emptyBuyQuoteParams); + const onChangeBuyQuotesParams = (e: ChangeEvent) => { + const { name, value } = e.target; + setBuyQuoteParams(prevState => { + return { + ...prevState, + [name]: value + } + }); + } + // Buy Quote Response + const [buyQuoteResponse, setBuyQuoteResponse] = useState(); + + + /* Wrapper around buy options API call under api/buy-options-api + - Calls and awaits the API with the current buyOptionsParams state + - Sets the buyOptionsResponse state to API response, reset buyQuoteParams, payment/purchase currencies + */ + + const buyOptionsWrapper = useCallback(async () => { + if (!buyOptionsParams.country) { + alert("Please fill out all required fields"); + return; + } + if (buyOptionsParams.country + buyOptionsParams.subdivision === prevCountrySubdiv.current) { // prevent re-fetching same data + return; + } + + const response = await generateBuyOptions(buyOptionsParams); + try { + setBuyOptionsResponse(response?.json); + setBuyQuoteParams({ + ...emptyBuyQuoteParams, + country: buyOptionsParams.country, + }); + + prevCountrySubdiv.current = buyOptionsParams.country + buyOptionsParams.subdivision; // store current query params for future caching + } catch (error) { + alert(error); + } + }, [buyOptionsParams]); + + const buyQuoteWrapper = useCallback(async () => { + if (!buyQuoteParams.purchase_currency || !buyQuoteParams.payment_currency || !buyQuoteParams.payment_method || !buyQuoteParams.payment_amount || !buyQuoteParams.country) { + alert("Please fill out all required fields"); + return; + } + if (parseInt(buyQuoteParams.payment_amount) < parseInt(payment_amount_limits.min) || + parseInt(buyQuoteParams.payment_amount) > parseInt(payment_amount_limits.max)) { + alert(`Payment amount for currency '${buyQuoteParams.payment_currency} - ${buyQuoteParams.payment_method}' must be between ${payment_amount_limits.min} and ${payment_amount_limits.max}`); + return; + } + + const response = await generateBuyQuote(buyQuoteParams); + try { + setBuyQuoteResponse(response); + } catch (error) { + alert(error); + } + }, [buyQuoteParams]); + + /* Change list of payment methods on re-render when new PAYMENT currency is changed */ + const payment_methods_list = useMemo(() => { + const methods = buyOptionsResponse?.payment_currencies.find(currency => currency.id === buyQuoteParams.payment_currency)?.limits.map(method => ({name: method.id})); + return methods || []; + }, [buyOptionsResponse, buyQuoteParams.payment_currency]); + + /* Change list of payment networks on re-render when new PURCHASE currency is changed */ + const purchase_networks_list = useMemo(() => { + const networks = buyOptionsResponse?.purchase_currencies.find(currency => currency.symbol === buyQuoteParams.purchase_currency)?.networks.map(method => ({name: method.name})); + return networks || []; + }, [buyOptionsResponse, buyQuoteParams.purchase_currency]); + + /* Change payment amount limits on re-render when changing PAYMENT currency & PAYMENT METHOD */ + const payment_amount_limits = useMemo(() => { + const limits = buyOptionsResponse?.payment_currencies + .find(currency => currency.id === buyQuoteParams.payment_currency) + ?.limits.find(limit => limit.id === buyQuoteParams.payment_method); + return { + min: limits?.min || '', + max: limits?.max || '', + }; + }, [buyOptionsResponse, buyQuoteParams.payment_currency, buyQuoteParams.payment_method]) + + return ( +
+
+ + {/* Generate Buy Configurations Card Box */} + + + + + {/* Buy Options Card Box */} + + {/* Buy Options Header */} +
+

scrollToHeader("buyOptionsHeader")} + className="font-bold"> + Generate Buy Options: +

+

The Buy Options API returns the supported fiat currencies and available crypto assets that can be passed into the Buy Quote API.

+
+ +
+

1. Input your country and optionally the subdivision, then click ‘Generate Buy Options’.

+

2. The response will show the payment and purchase options in your selected country/subdivision. Your selected country will be passed into the Buy Quote API.

+
+ + + {/* Buy Options API Request Parameters & Button to generate Buy Options */} +
+
+

Enter Request Parameters

+ {onChangeBuyOptionsParams(value)}} + isRequired + /> + {onChangeBuyOptionsParams(value)}} + /> + +
+ +
+

Buy Options Response

+ + {buyOptionsResponse && } + +
+
+
+
+ + + + {/* Generate Buy Quote Card Box */} +
+ + {/* Buy Quote Header */} +
+

scrollToHeader("buyQuoteHeader")} className="font-bold"> Generate Buy Quote:

+

+ The Buy Quote API provides clients with a quote based on the asset the user would like to purchase, + the network they plan to purchase it on, the dollar amount of the payment, the payment currency, + the payment method, and country of the user. +

+
+
+

1. ’Generate Buy Options’ in the section above to specify the country parameter.

+

2. Select a payment currency, then select a payment method based on the available options.

+

3. Select a purchase currency, then optionally select a purchase network.

+

4. Enter the fiat payment amount you wish to spend on this transaction. Then, click ‘Generate Buy Quote’ .

+

5. The quoteID and Buy Quote request parameters will be passed into your one-time Coinbase Onramp URL in the section below.

+
+
+ {/* Country, Purchase Currency, Payment Currency, Payment Method, Network, Amount Options */} +
+ +

Enter Request Parameters

+ + +
+ + + + + + +
+ +
+ + + + + + +
+ + + {onChangeBuyQuotesParams(value)}} + isRequired + isDisabled={buyQuoteParams.payment_currency === '' || buyQuoteParams.payment_method === ''} + /> + + {/* Generate Buy Quote Button */} + +
+ + {/* Buy Quote Response */} +
+

Buy Quote Response

+ + {buyQuoteResponse && } + +
+
+
+ + + + {/* Generate Secure Onramp Token + URL Card Box */} + network.name)} + /> +
+
+ ) +} \ No newline at end of file diff --git a/app/components/SecureTokenBox.tsx b/app/components/SecureTokenBox.tsx new file mode 100644 index 0000000..b488882 --- /dev/null +++ b/app/components/SecureTokenBox.tsx @@ -0,0 +1,150 @@ +import { Input } from "@nextui-org/input"; +import { Code } from "@nextui-org/code"; +import { Button } from "@nextui-org/button"; +import { Textarea } from "@nextui-org/input"; +import { useState, useCallback, useMemo, ChangeEvent } from "react"; + +import {Card, Link, Select, SelectItem} from "@nextui-org/react"; +import { AggregatorInputParams} from "../utils/types"; +import { generateSecureToken } from "../utils/queries"; +import {BLOCKCHAIN_LIST} from "../utils/blockchains"; + +export default function SecureTokenBox({ aggregatorInputs, showBuyQuoteURLText, blockchains }: { aggregatorInputs?: AggregatorInputParams, showBuyQuoteURLText?: boolean, blockchains?: string[]}) { + const [secureToken, setSecureToken] = useState(""); + const [ethAddress, setEthAddress] = useState(""); + + const [blockchainOption, setBlockchainOption] = useState(""); + + const setBlockchain = useCallback((event: ChangeEvent) => { + setBlockchainOption(event.target.value); + }, []); + + const secureTokenWrapper = useCallback(async () => { + const response = await generateSecureToken({ethAddress, blockchains: showBuyQuoteURLText ? blockchains : [blockchainOption.toLowerCase()]}) + console.log("generateSecureToken"); + try { + if (response) {setSecureToken(response);} else {setSecureToken('')} + } catch (error) { + alert(error); + console.error(error); + }}, [ethAddress, showBuyQuoteURLText, blockchains, blockchainOption]); + + + // fetch("/api/secure-token", { + // method: "POST", + // body: JSON.stringify({ ethAddress, blockchains: blockchains || [blockchainOption]}), + // }) + // .then(async (response) => { + // const json = await response.json(); + // if(response.ok) { + // setSecureToken(json.token); + // } else { + // alert("Error generating token: "+json.error); + // console.log("Error generating token: "+json.error); + // } + // }); + // }, [ethAddress, blockchains]); + + const linkReady = useMemo(() => secureToken.length > 0, [secureToken]); + + const link = useMemo(() => { + if (!linkReady) return "Generate a secure token first to create your one time URL"; + return ( + "https://pay.coinbase.com/buy/select-asset?sessionToken=" + secureToken + + (aggregatorInputs?.quoteID ? ""eId=" + aggregatorInputs.quoteID : "") + + (aggregatorInputs?.defaultAsset ? "&defaultAsset=" + aggregatorInputs.defaultAsset : "") + + (aggregatorInputs?.defaultPaymentMethod ? "&defaultPaymentMethod=" + aggregatorInputs.defaultPaymentMethod : "") + + (aggregatorInputs?.defaultNetwork ? "&defaultNetwork=" + aggregatorInputs.defaultNetwork : "") + + (aggregatorInputs?.fiatCurrency ? "&fiatCurrency=" + aggregatorInputs.fiatCurrency : "") + + (aggregatorInputs?.presentFiatAmount ? "&presetFiatAmount=" + aggregatorInputs.presentFiatAmount : "") + ); + }, [linkReady, secureToken, aggregatorInputs]); + + const launch = useCallback(() => { + open(link, "_blank", "popup,width=540,height=700") + }, [link]); + + const helperText = showBuyQuoteURLText ? +

The generated link initializes the Coinbase Onramp URL with the appropriate parameters to execute that buy in just one click for the user.

: +

Generate a secure one time URL to launch an Onramp session.

+ + const buyQuoteURLDirections = ( +
+

1. Generate a Buy Quote in the section above to get the input parameters to create a secure Onramp URL.

+

2. Enter a destination wallet address and then click ‘Generate secure token’.

+

3. Click Launch Onramp to see the one-click buy experience for your users.

+
+ ) + + return ( + +
+

Generate Secure Onramp Token & URL:

+ {helperText} +
+ {showBuyQuoteURLText && buyQuoteURLDirections} + +
+
+ { + setEthAddress(value); + setSecureToken(""); + }} + isRequired + /> + + {!showBuyQuoteURLText && + } + + + + {secureToken.length > 0 && ( + <> +

Onramp token:

+ {secureToken} + + )} +
+ + +
+