Skip to content

Commit

Permalink
Merge pull request #1 from wbt/wbt-ui1
Browse files Browse the repository at this point in the history
Early merchant UI
  • Loading branch information
wbt authored Oct 22, 2023
2 parents 53c5146 + 1679836 commit 960bf58
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 3 deletions.
32 changes: 30 additions & 2 deletions packages/hardhat/contracts/SubscryptoToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion packages/nextjs/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -17,6 +17,11 @@ export const menuLinks: HeaderMenuLink[] = [
label: "Home",
href: "/",
},
{
label: "Merchant: Set up tiers",
href: "/merchant",
icon: <BuildingStorefrontIcon className="h-4 w-4" />,
},
{
label: "Debug Contracts",
href: "/debug",
Expand Down
39 changes: 39 additions & 0 deletions packages/nextjs/components/example-ui/TierCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<li className="py-8 px-5 border border-primary rounded-xl flex m-5">
Calls/activities per week maximum:{" "}
{unitsIsLoading ? "Loading..." : typeof unitsPerWeek === "undefined" ? "*" : unitsPerWeek.toString()}
<br />
Price per week in credits (each ≈$1):{" "}
{priceIsLoading ? "Loading..." : typeof pricePerWeek === "undefined" ? "*" : formatEther(pricePerWeek)}
<br />
{props.showOfferedStatus ? activeOfferText : null}
</li>
);
};
24 changes: 24 additions & 0 deletions packages/nextjs/components/example-ui/TierList.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col justify-center items-center py-10 px-5 sm:px-0 lg:py-auto max-w-[100vw] ">
<div className="flex justify-between w-full">
<ol>
{indicies.map(index => (
<TierCard key={index.toString()} merchant={merchant} tierIndex={index} showOfferedStatus={true} />
))}
</ol>
</div>
</div>
);
};
66 changes: 66 additions & 0 deletions packages/nextjs/components/example-ui/TierListing.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="w-full">
<div className="topRow">
<h2 className="text-4xl">{tierCountText}</h2>
</div>
<TiersList merchant={address} tiersLength={tiersLength} />
</div>
);
};
57 changes: 57 additions & 0 deletions packages/nextjs/components/example-ui/TierSetup.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex bg-base-300 relative pb-10">
<div className="flex flex-col w-full mx-5 sm:mx-8 2xl:mx-20">
<div className="flex flex-col mt-6 px-7 py-8 bg-base-200 opacity-80 rounded-2xl shadow-lg border-2 border-primary">
<span className="text-xl my-4">Create a new service tier:</span>
<input
type="number"
placeholder="Calls/activities per week maximum"
className="input w-full px-5 border border-primary text-lg my-4"
onChange={e => setUnitsPerWeek(e.target.value)}
/>
<br />
<input
type="number"
placeholder="Price per week in credits (each ≈$1)"
className="input w-full px-5 border border-primary text-lg my-4"
onChange={e => setPricePerWeek(e.target.value)}
/>
<div className="flex rounded-full p-1 flex-shrink-0 place-content-end">
<button
className="btn btn-primary rounded-full capitalize font-normal font-white flex items-center gap-1 hover:gap-2 transition-all tracking-widest"
onClick={() => writeAsync()}
disabled={isLoading}
>
{isLoading ? (
<span className="loading loading-spinner loading-sm"></span>
) : (
<>
Create tier <ArrowSmallRightIcon className="w-3 h-3 mt-0.5" />
</>
)}
</button>
</div>
</div>
</div>
</div>
);
};
21 changes: 21 additions & 0 deletions packages/nextjs/pages/merchant.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<MetaHeader
title="API Merchant: Set up tiers"
description="Allows the maker of an API to set up offerings to sell access for cryptocurrency payments."
/>
<div className="grid lg:grid-cols-2 flex-grow" data-theme="exampleUi">
<TierListing />
<TierSetup />
</div>
</>
);
};

export default MerchantSetup;

0 comments on commit 960bf58

Please sign in to comment.