diff --git a/packages/hardhat/contracts/SubscryptoToken.sol b/packages/hardhat/contracts/SubscryptoToken.sol index 06c62c5..70d7759 100644 --- a/packages/hardhat/contracts/SubscryptoToken.sol +++ b/packages/hardhat/contracts/SubscryptoToken.sol @@ -149,6 +149,33 @@ contract SubscryptoToken is ERC20, ERC20Burnable, Ownable, ERC20Permit { _mint(to, amount); } + function getTiersCount( + address merchant + ) public view returns(uint) { + return tiersOffered[merchant].length; + } + + function getTierUnitsPerWeek( + address merchant, + uint tierIndex + ) public view returns(uint) { + return tiersOffered[merchant][tierIndex].unitsPerWeek; + } + + function getTierPricePerWeek( + address merchant, + uint tierIndex + ) public view returns(uint) { + return tiersOffered[merchant][tierIndex].pricePerWeek; + } + + function getTierisActivelyOffered( + address merchant, + uint tierIndex + ) public view returns(bool) { + return tiersOffered[merchant][tierIndex].isActivelyOffered; + } + function addTier( uint unitsPerWeek, uint pricePerWeek @@ -165,7 +192,7 @@ contract SubscryptoToken is ERC20, ERC20Burnable, Ownable, ERC20Permit { uint pricePerWeek, bool isActivelyOffered ) public { - if (tiersOffered[msg.sender]).length == 0 { + if (tiersOffered[msg.sender].length == 0) { //Set index 0 to the non-subscription: tiersOffered[msg.sender].push(Tier({ unitsPerWeek: 0, @@ -279,7 +306,8 @@ contract SubscryptoToken is ERC20, ERC20Burnable, Ownable, ERC20Permit { address customer, uint tierIndex ) private { - require(tierIndex < tiers[merchant].length, 'No such tier offered by this merchant.'); + require(tierIndex < tiersOffered[merchant].length, 'No such tier offered by this merchant.'); + require(tiersOffered[merchant][tierIndex].isActivelyOffered, 'Tier is not actively offered at present.'); accountAtSubscriptionEnd(merchant, customer); subscriptions[merchant][customer] = Subscription({ tier: tierIndex, diff --git a/packages/nextjs/components/Header.tsx b/packages/nextjs/components/Header.tsx index 52a7889..24217f1 100644 --- a/packages/nextjs/components/Header.tsx +++ b/packages/nextjs/components/Header.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useRef, useState } from "react"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import { Bars3Icon, BugAntIcon, SparklesIcon } from "@heroicons/react/24/outline"; +import { Bars3Icon, BugAntIcon, BuildingStorefrontIcon, SparklesIcon } from "@heroicons/react/24/outline"; import { FaucetButton, RainbowKitCustomConnectButton } from "~~/components/scaffold-eth"; import { useOutsideClick } from "~~/hooks/scaffold-eth"; @@ -17,6 +17,11 @@ export const menuLinks: HeaderMenuLink[] = [ label: "Home", href: "/", }, + { + label: "Merchant: Set up tiers", + href: "/merchant", + icon: , + }, { label: "Debug Contracts", href: "/debug", diff --git a/packages/nextjs/components/example-ui/TierCard.tsx b/packages/nextjs/components/example-ui/TierCard.tsx new file mode 100644 index 0000000..f2aec89 --- /dev/null +++ b/packages/nextjs/components/example-ui/TierCard.tsx @@ -0,0 +1,39 @@ +import { formatEther } from "viem"; +import { useScaffoldContractRead } from "~~/hooks/scaffold-eth"; + +export const TierCard = (props: { merchant: string; tierIndex: bigint; showOfferedStatus: boolean }) => { + const { data: unitsPerWeek, isLoading: unitsIsLoading } = useScaffoldContractRead({ + contractName: "SubscryptoToken", + functionName: "getTierUnitsPerWeek", + args: [props.merchant, props.tierIndex], + }); + const { data: pricePerWeek, isLoading: priceIsLoading } = useScaffoldContractRead({ + contractName: "SubscryptoToken", + functionName: "getTierPricePerWeek", + args: [props.merchant, props.tierIndex], + }); + const { data: isActivelyOffered, isLoading: activeStatusIsLoading } = useScaffoldContractRead({ + contractName: "SubscryptoToken", + functionName: "getTierisActivelyOffered", + args: [props.merchant, props.tierIndex], + }); + + let activeOfferText = "Actively offered"; + if (activeStatusIsLoading) { + activeOfferText = "Checking to see if this is actively offered."; + } else if (!isActivelyOffered) { + activeOfferText = "Not actively offered."; + } + + return ( +
  • + Calls/activities per week maximum:{" "} + {unitsIsLoading ? "Loading..." : typeof unitsPerWeek === "undefined" ? "*" : unitsPerWeek.toString()} +
    + Price per week in credits (each ≈$1):{" "} + {priceIsLoading ? "Loading..." : typeof pricePerWeek === "undefined" ? "*" : formatEther(pricePerWeek)} +
    + {props.showOfferedStatus ? activeOfferText : null} +
  • + ); +}; diff --git a/packages/nextjs/components/example-ui/TierList.tsx b/packages/nextjs/components/example-ui/TierList.tsx new file mode 100644 index 0000000..f875f24 --- /dev/null +++ b/packages/nextjs/components/example-ui/TierList.tsx @@ -0,0 +1,24 @@ +import { TierCard } from "./TierCard"; + +export const TiersList = (props: { merchant?: string; tiersLength?: bigint }) => { + const tiersLength = props.tiersLength; + const merchant = props.merchant; + if (typeof tiersLength == "undefined" || typeof merchant == "undefined") { + return null; + } + const indicies = []; + for (let i = 1n; i < tiersLength; i++) { + indicies.push(i); + } + return ( +
    +
    +
      + {indicies.map(index => ( + + ))} +
    +
    +
    + ); +}; diff --git a/packages/nextjs/components/example-ui/TierListing.tsx b/packages/nextjs/components/example-ui/TierListing.tsx new file mode 100644 index 0000000..f4d2c83 --- /dev/null +++ b/packages/nextjs/components/example-ui/TierListing.tsx @@ -0,0 +1,66 @@ +import { TiersList } from "./TierList"; +import { useAccount } from "wagmi"; +import { + useScaffoldContract, + useScaffoldContractRead, + useScaffoldEventHistory, + useScaffoldEventSubscriber, +} from "~~/hooks/scaffold-eth"; + +export const TierListing = () => { + const { address } = useAccount(); + + const { data: tiersLength, isLoading: isTierCountLoading } = useScaffoldContractRead({ + contractName: "SubscryptoToken", + functionName: "getTiersCount", + args: [address], + }); + + useScaffoldEventSubscriber({ + contractName: "SubscryptoToken", + eventName: "TierAdded", + listener: logs => { + logs.map(log => { + const { merchant, tierIndex, unitsPerWeek, pricePerWeek, isActivelyOffered } = log.args; + console.log("📡 TierAdded event", merchant, tierIndex, unitsPerWeek, pricePerWeek, isActivelyOffered); + }); + }, + }); + + const { + data: myGreetingChangeEvents, + isLoading: isLoadingEvents, + error: errorReadingEvents, + } = useScaffoldEventHistory({ + contractName: "SubscryptoToken", + eventName: "TierAdded", + fromBlock: process.env.NEXT_PUBLIC_DEPLOY_BLOCK ? BigInt(process.env.NEXT_PUBLIC_DEPLOY_BLOCK) : 0n, + filters: { merchant: address }, + blockData: true, + }); + + console.log("Events:", isLoadingEvents, errorReadingEvents, myGreetingChangeEvents); + + const { data: yourContract } = useScaffoldContract({ contractName: "SubscryptoToken" }); + console.log("subscryptoToken: ", yourContract); + + let tierCountText = "No defined tiers for this merchant account yet! Use the tool to the right to create one."; + if (isTierCountLoading) { + tierCountText = "Loading..."; + } else if (typeof tiersLength === "undefined") { + tierCountText = "Error loading defined tiers."; + } else if (tiersLength === 1n) { + tierCountText = (tiersLength - 1n).toString() + " defined tier:"; + } else if (tiersLength > 1n) { + tierCountText = (tiersLength - 1n).toString() + " defined tiers:"; + } + + return ( +
    +
    +

    {tierCountText}

    +
    + +
    + ); +}; diff --git a/packages/nextjs/components/example-ui/TierSetup.tsx b/packages/nextjs/components/example-ui/TierSetup.tsx new file mode 100644 index 0000000..7bda575 --- /dev/null +++ b/packages/nextjs/components/example-ui/TierSetup.tsx @@ -0,0 +1,57 @@ +import { useState } from "react"; +import { parseEther } from "viem"; +import { ArrowSmallRightIcon } from "@heroicons/react/24/outline"; +import { useScaffoldContractWrite } from "~~/hooks/scaffold-eth"; + +export const TierSetup = () => { + const [unitsPerWeek, setUnitsPerWeek] = useState(""); + const [pricePerWeek, setPricePerWeek] = useState(""); + + const { writeAsync, isLoading } = useScaffoldContractWrite({ + contractName: "SubscryptoToken", + functionName: "addTier", + //@ts-ignore: not sure why it's not picking up that this function exists on this contract + args: [parseInt(unitsPerWeek), parseEther(pricePerWeek)], + onBlockConfirmation: txnReceipt => { + console.log("📦 Transaction blockHash", txnReceipt.blockHash); + }, + }); + + return ( +
    +
    +
    + Create a new service tier: + setUnitsPerWeek(e.target.value)} + /> +
    + setPricePerWeek(e.target.value)} + /> +
    + +
    +
    +
    +
    + ); +}; diff --git a/packages/nextjs/pages/merchant.tsx b/packages/nextjs/pages/merchant.tsx new file mode 100644 index 0000000..f8e6d08 --- /dev/null +++ b/packages/nextjs/pages/merchant.tsx @@ -0,0 +1,21 @@ +import type { NextPage } from "next"; +import { MetaHeader } from "~~/components/MetaHeader"; +import { TierListing } from "~~/components/example-ui/TierListing"; +import { TierSetup } from "~~/components/example-ui/TierSetup"; + +const MerchantSetup: NextPage = () => { + return ( + <> + +
    + + +
    + + ); +}; + +export default MerchantSetup;