Ex 4. IOTA Rebased Contract ClubMem

unofficial site

- return to IOTA Rebased Useful Links

ClubMem Dapp

Context

A contract has already been deployed on the IOTA Rebased Testnet. That has been tested using the IOTA CLI. Now a Dapp is developed to allow others to access the contract through a webpage where they can use their IOTA Rebased Wallet which is a Chrome browser extension.

Important. IOTA DAPP Template. To get started building a Dapp this very useful tool was used. This is a link to the official IOTA docs page https://docs.iota.org/references/ts-sdk/dapp-kit/create-dapp

. You may also find this Github useful:
https://github.com/iotaledger/iota/tree/develop/sdk/dapp-kit.

Here is another useful link: www.npmjs.com/package/@iota/create-dapp

One option is, on the command line enter:

% npm create @iota/dapp

This message results:

Which starter template would you like to use? …
react-client-dapp React Client dApp that reads data from wallet and the blockchain
react-e2e-counter React dApp with a move smart contract that implements a distributed counter


The second question follows:
? What is the name of your dApp? (this will be used as the directory name)

This project started with the first template which gives inbuilt functionality to have the IOTA Chrome Extension wallet connect and sign transactions. The template has a ReadMe file with useful information about its use of React, Typescript, Vite and the IOTA Dapp Kit.

To run the default project use:

% npm install

At one point the version of the template tool was updated and it was useful to clear the npx cache to ensure the latest version was in play - for which this was useful: npx clear-npx-cache

Next step is to start the development server at somewhere like http://localhost:5173/ with:

% npm run dev

[1] Overview of this project

This is the first phase of the 'Club membership' development. It simply allows somebody to select one of three membership classes and pay for them accordingly. The funds go to a 'Treasury' account and a confirmation of membership is shown on screen. The underlying contract being published on the IOTA Testnet with Package ID: 0x36e7624d6afeead61cd6971193d5d1544a7f5efe04db530a43f61f67d1d40f69

ClubMem Dapp on Netlify

If still up (!) then the link is: clubmem.netlify.app

[2] Main TSX files etc

App.tsx for ClubMem Dapp


<import { ConnectButton } from "@iota/dapp-kit">
<import { Box, Container, Flex, Heading, Text, Card, Button } from "@radix-ui/themes">
<import { WalletStatus } from "./WalletStatus">
<import { MembershipCall } from "./MembershipCall">
<import { CheckCircle2, ExternalLink } from 'lucide-react'>
<import { useState } from 'react'>

interface MembershipTier {
  name: string;
  cost: number;
  description: string;
}

interface TransactionStatus {
  success: boolean;
  digest?: string;
  message?: string;
  status?: any;
}

function App() {
  const [selectedTier, setSelectedTier] = useState<string>('');
  const [isConfirming, setIsConfirming] = useState(false);
  const [isCompleted, setIsCompleted] = useState(false);
  const [transactionStatus, setTransactionStatus] = useState<TransactionStatus | null>(null);

  const membershipTiers: MembershipTier[] = [
    { 
      name: "Gold", 
      cost: 5, 
      description: "Gold Membership - Premium benefits"
    },
    { 
      name: "Silver", 
      cost: 3, 
      description: "Silver Membership - Enhanced access"
    },
    { 
      name: "Bronze", 
      cost: 2, 
      description: "Bronze Membership - Basic benefits"
    }
  ];

  const handleTierSelect = (tierName: string) => {
    setSelectedTier(tierName);
    setIsConfirming(true);
  };

  const handleBack = () => {
    if (!isCompleted) {
      setSelectedTier('');
      setIsConfirming(false);
    }
  };

  const handleTransactionComplete = (status: TransactionStatus) => {
    console.log("Setting transaction status in App:", status);
    setTransactionStatus(status);
    setIsCompleted(true);
  };

  const renderTransactionSuccess = () => {
    if (!transactionStatus || !transactionStatus.success) return null;

    return (
      <Box style={{ 
        border: '1px solid #4CAF50',
        padding: '24px', 
        borderRadius: '8px',
        backgroundColor: '#F1F8E9'
      }}>
        <Flex direction="column" align="center" gap="4">
          <CheckCircle2 color="#4CAF50" size={48} />
          <Text size="6" weight="bold" style={{ color: '#2E7D32' }}>
            {selectedTier} Membership Activated!
          </Text>
          <Text size="3" align="center">
            Welcome to ClubMem! Your membership is now active.
          </Text>
          {transactionStatus.digest && (
            <Box style={{ width: '100%', marginTop: '16px' }}>
              <Text size="2" weight="bold">Transaction Details:</Text>
              <Box style={{ 
                backgroundColor: 'white',
                padding: '12px',
                borderRadius: '4px',
                marginTop: '8px',
                wordBreak: 'break-all',
                color: '#333333'
              }}>
                <Text size="2"><strong>Transaction ID:</strong> {transactionStatus.digest}</Text>
              </Box>
              <Box mt="3">
                <a
                  href={`https://explorer.rebased.iota.org/txblock/${transactionStatus.digest}`}
                  target="_blank"
                  rel="noopener noreferrer"
                  style={{ 
                    display: 'inline-flex',
                    alignItems: 'center',
                    gap: '4px',
                    color: '#1976D2',
                    textDecoration: 'none'
                  }}
                >
                  <ExternalLink size={14} />
                  <Text size="2">View in Explorer</Text>
                </a>
              </Box>
            </Box>
          )}
        </Flex>
      </Box>
    );
  };

  return (
    <>
      <Flex
        position="sticky"
        px="4"
        py="2"
        justify="between"
        style={{
          borderBottom: "1px solid var(--gray-a2)",
        }}
      >
        <Box>
          <Heading>ClubMem Membership Portal</Heading>
        </Box>
        <Box>
          <ConnectButton />
        </Box>
      </Flex>

      <Container>
        <Container
          mt="5"
          pt="2"
          px="4"
          style={{ background: "var(--gray-a2)", minHeight: 500 }}
        >
          <Card mb="4">
            <Heading size="4" mb="2">Welcome to ClubMem</Heading>
            {!isCompleted && (
              <>
                <Text as="p" mb="2">
                  Join our exclusive membership program by selecting your preferred tier below.
                </Text>
                <Text as="p" mb="2">
                  Steps to become a member:
                </Text>
                <Box ml="4">
                  <Text as="p" mb="1">1. Connect your IOTA wallet using the button in the top right</Text>
                  <Text as="p" mb="1">2. Select your desired membership tier</Text>
                  <Text as="p">3. Confirm the payment in your wallet</Text>
                </Box>
              </>
            )}
          </Card>

          <WalletStatus />

          <Card mt="4">
            {isCompleted && transactionStatus ? (
              renderTransactionSuccess()
            ) : !isConfirming ? (
              <>
                <Heading size="3" mb="2">Select Membership Tier</Heading>
                <Flex direction="column" gap="2" style={{ maxWidth: '400px', margin: '0 auto' }}>
                  {membershipTiers.map((tier) => (
                    <Button 
                      key={tier.name}
                      size="3"
                      variant="soft"
                      onClick={() => handleTierSelect(tier.name)}
                      style={{
                        padding: '16px',
                        justifyContent: 'space-between',
                        display: 'flex',
                        alignItems: 'center',
                        cursor: 'pointer'
                      }}
                    >
                      <Text size="2" weight="bold">{tier.name}</Text>
                      <Text size="2">{tier.cost} IOTA</Text>
                    </Button>
                  ))}
                </Flex>
              </>
            ) : (
              <Box>
                <Flex justify="between" align="center" mb="4">
                  <Heading size="3">Confirm Membership</Heading>
                  <Button 
                    variant="soft" 
                    onClick={handleBack}
                    size="2"
                  >
                    Back
                  </Button>
                </Flex>
                
                {selectedTier && (
                  <MembershipCall 
                    tier={selectedTier}
                    amount={membershipTiers.find(t => t.name === selectedTier)?.cost || 0}
                    onComplete={handleTransactionComplete}
                  />
                )}
              </Box>
            )}
          </Card>
        </Container>
      </Container>
    </>
  );
}

export default App;

MembershipCall.tsx for ClubMem Dapp


<import {
  useCurrentAccount,
  useSignAndExecuteTransaction,
  useIotaClient,
} from "@iota/dapp-kit">
<import { Transaction } from "@iota/iota-sdk/transactions">
<import { Button, Container, Text, Card, Box, Flex } from "@radix-ui/themes">
<import { Loader2 } from 'lucide-react'>
<import { useState } from 'react'>

interface MembershipCallProps {
  tier: string;
  amount: number;
  onComplete: (status: any) => void;
}

export function MembershipCall({ tier, amount, onComplete }: MembershipCallProps) {
  const packageId = "0x36e7624d6afeead61cd6971193d5d1544a7f5efe04db530a43f61f67d1d40f69";
  const iotaClient = useIotaClient();
  const currentAccount = useCurrentAccount();
  const [isLoading, setIsLoading] = useState(false);
  const { mutate: signAndExecute } = useSignAndExecuteTransaction({
    execute: async ({ bytes, signature }) =>
      await iotaClient.executeTransactionBlock({
        transactionBlock: bytes,
        signature,
        options: {
          showRawEffects: true,
          showEffects: true,
        },
      }),
  });

  const makePayment = () => {
    console.log("Starting payment process");
    setIsLoading(true);
    try {
      const tx = new Transaction();
      const amountInNanos = amount * 1_000_000_000;
      const [paymentCoin] = tx.splitCoins(tx.gas, [tx.pure.u64(amountInNanos)]);
      tx.moveCall({
        target: `${packageId}::membership::join`,
        typeArguments: ["0x2::iota::IOTA"],
        arguments: [paymentCoin]
      });
      signAndExecute(
        {
          transaction: tx,
        },
        {
          onSuccess: (result) => {
            console.log("Transaction result:", result);
            
            const status = {
              success: true,
              digest: result.digest,
              message: `Your ${tier} membership has been confirmed!`,
              status: result.effects?.status
            };
            
            setIsLoading(false);
            onComplete(status);
          },
          onError: (error) => {
            console.error("Transaction error:", error);
            onComplete({
              success: false,
              message: error.message || 'Transaction failed'
            });
            setIsLoading(false);
          }
        },
      );
    } catch (error) {
      console.error("Setup error:", error);
      onComplete({
        success: false,
        message: 'Failed to set up transaction'
      });
      setIsLoading(false);
    }
  };

  return (
    <Container my="4">
      <Card style={{ overflow: 'visible' }}>
        <Box p="4">
          {isLoading ? (
            <Flex direction="column" align="center" gap="3">
              <Loader2 className="animate-spin" size={24} />
              <Text>Processing your {tier} membership...</Text>
            </Flex>
          ) : (
            <Button
              onClick={makePayment}
              size="3"
              disabled={!currentAccount}
              style={{ width: '100%' }}
            >
              Confirm {tier} Membership - {amount} IOTA
            </Button>
          )}
        </Box>
      </Card>
    </Container>
  );
}

WalletStatus.tsx for ClubMem Dapp


<import { useCurrentAccount } from "@iota/dapp-kit">
<import { Container, Flex, Heading, Text } from "@radix-ui/themes">

export function WalletStatus() {
  const account = useCurrentAccount();
  return (
    <Container my="2">
      <Heading mb="2">Wallet Status</Heading>
      {account ? (
        <Flex direction="column">
          <Text>Wallet connected</Text>
          <Text>Address: {account.address}</Text>
        </Flex>
      ) : (
        <Text>Wallet not connected</Text>
      )}
    </Container>
  );
}