From d54d0dfebd50e39e5097b1c34d65bf516a28b597 Mon Sep 17 00:00:00 2001
From: aowheel <aoimakino2003@gmail.com>
Date: Tue, 21 Jan 2025 09:45:25 +0900
Subject: [PATCH 1/2] =?UTF-8?q?=E6=A9=9F=E8=83=BD=E5=AE=9F=E8=A3=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 pkgs/frontend/app/root.tsx                    |  29 ++-
 ...d_.$hatId_.$address_.assistcredit.send.tsx | 222 +++++++++---------
 pkgs/frontend/hooks/useFractionToken.ts       |  11 +-
 pkgs/frontend/package.json                    |   2 +
 4 files changed, 151 insertions(+), 113 deletions(-)

diff --git a/pkgs/frontend/app/root.tsx b/pkgs/frontend/app/root.tsx
index c10965c..131df05 100644
--- a/pkgs/frontend/app/root.tsx
+++ b/pkgs/frontend/app/root.tsx
@@ -3,13 +3,20 @@ import { Container } from "@chakra-ui/react";
 import { withEmotionCache } from "@emotion/react";
 import { PrivyProvider } from "@privy-io/react-auth";
 import {
+  type ClientLoaderFunctionArgs,
   Links,
   Meta,
   Outlet,
   Scripts,
   ScrollRestoration,
+  data,
+  useLoaderData,
 } from "@remix-run/react";
 import { currentChain } from "hooks/useViem";
+import { useEffect } from "react";
+import { ToastContainer, toast as notify } from "react-toastify";
+import toastStyles from "react-toastify/ReactToastify.css?url";
+import { getToast } from "remix-toast";
 import { goldskyClient } from "utils/apollo";
 import { Header } from "./components/Header";
 import { SwitchNetwork } from "./components/SwitchNetwork";
@@ -45,14 +52,33 @@ export const Layout = withEmotionCache((props: LayoutProps, cache) => {
   );
 });
 
+// Add the toast stylesheet
+export const links = () => [{ rel: "stylesheet", href: toastStyles }];
+// Implemented from above
+export const loader = async ({ request }: ClientLoaderFunctionArgs) => {
+  const { toast, headers } = await getToast(request);
+  return data({ toast }, { headers });
+};
+
 export default function App() {
+  const {
+    data: { toast },
+  } = useLoaderData<typeof loader>();
+  // Hook to show the toasts
+  useEffect(() => {
+    if (toast) {
+      // notify on a toast message
+      notify(toast.message, { type: toast.type });
+    }
+  }, [toast]);
+
   return (
     <ApolloProvider client={goldskyClient}>
       <PrivyProvider
         appId={import.meta.env.VITE_PRIVY_APP_ID}
         config={{
           appearance: {
-            walletList: ["coinbase_wallet"],
+            walletList: ["coinbase_wallet", "metamask"],
           },
           embeddedWallets: {
             createOnLogin: "users-without-wallets",
@@ -77,6 +103,7 @@ export default function App() {
             <Header />
             <Outlet />
           </Container>
+          <ToastContainer />
         </ChakraProvider>
       </PrivyProvider>
     </ApolloProvider>
diff --git a/pkgs/frontend/app/routes/$treeId_.$hatId_.$address_.assistcredit.send.tsx b/pkgs/frontend/app/routes/$treeId_.$hatId_.$address_.assistcredit.send.tsx
index 222e7fe..400b874 100644
--- a/pkgs/frontend/app/routes/$treeId_.$hatId_.$address_.assistcredit.send.tsx
+++ b/pkgs/frontend/app/routes/$treeId_.$hatId_.$address_.assistcredit.send.tsx
@@ -1,5 +1,6 @@
 import {
   Box,
+  Button,
   Flex,
   Float,
   Grid,
@@ -24,6 +25,7 @@ import { useTreeInfo } from "hooks/useHats";
 import { type NameData, TextRecords } from "namestone-sdk";
 import { type FC, useCallback, useEffect, useMemo, useState } from "react";
 import { FaArrowRight } from "react-icons/fa6";
+import { toast } from "react-toastify";
 import { ipfs2https } from "utils/ipfs";
 import { abbreviateAddress } from "utils/wallet";
 import type { Address } from "viem";
@@ -93,7 +95,9 @@ const AssistCreditSend: FC = () => {
         receiver.address as Address,
         BigInt(amount),
       );
-      res && navigate(`/${treeId}/${hatId}/${address}`);
+      console.log(res);
+      res?.error && toast.error(res.error);
+      res?.txHash && navigate(`/${treeId}/${hatId}/${address}`);
     } catch (error) {
       console.error(error);
     }
@@ -110,117 +114,121 @@ const AssistCreditSend: FC = () => {
   ]);
 
   return (
-    <Grid
-      gridTemplateRows={!receiver ? "auto auto auto 1fr" : "auto auto 1fr auto"}
-      minH="calc(100vh - 100px)"
-    >
-      <PageHeader
-        title={
-          receiver
-            ? `${receiver.name || `${abbreviateAddress(receiver.address)}に送信`}`
-            : "アシストクレジット送信"
+    <>
+      <Grid
+        gridTemplateRows={
+          !receiver ? "auto auto auto 1fr" : "auto auto 1fr auto"
         }
-        backLink={
-          receiver &&
-          (() => {
-            setReceiver(undefined);
-            setAmount(0);
-          })
-        }
-      />
-
-      <HStack my={2}>
-        <RoleIcon size="50px" />
-        <Text>掃除当番(残高: {balanceOfToken?.toLocaleString()})</Text>
-      </HStack>
-
-      {!receiver ? (
-        <>
-          <Field label="ユーザー名 or ウォレットアドレスで検索">
-            <CommonInput
-              value={searchText}
-              onChange={(e) => {
-                setSearchText(e.target.value);
-              }}
-              placeholder="ユーザー名 or ウォレットアドレス"
-            />
-          </Field>
-
-          <List.Root listStyle="none" my={10} gap={3}>
-            {users?.flat().map((user, index) => (
-              <List.Item
-                key={`${user.name}u`}
-                onClick={() => setReceiver(user)}
-              >
-                <HStack>
+        minH="calc(100vh - 100px)"
+      >
+        <PageHeader
+          title={
+            receiver
+              ? `${receiver.name || `${abbreviateAddress(receiver.address)}に送信`}`
+              : "アシストクレジット送信"
+          }
+          backLink={
+            receiver &&
+            (() => {
+              setReceiver(undefined);
+              setAmount(0);
+            })
+          }
+        />
+
+        <HStack my={2}>
+          <RoleIcon size="50px" />
+          <Text>掃除当番(残高: {balanceOfToken?.toLocaleString()})</Text>
+        </HStack>
+
+        {!receiver ? (
+          <>
+            <Field label="ユーザー名 or ウォレットアドレスで検索">
+              <CommonInput
+                value={searchText}
+                onChange={(e) => {
+                  setSearchText(e.target.value);
+                }}
+                placeholder="ユーザー名 or ウォレットアドレス"
+              />
+            </Field>
+
+            <List.Root listStyle="none" my={10} gap={3}>
+              {users?.flat().map((user, index) => (
+                <List.Item
+                  key={`${user.name}u`}
+                  onClick={() => setReceiver(user)}
+                >
+                  <HStack>
+                    <UserIcon
+                      userImageUrl={ipfs2https(user.text_records?.avatar)}
+                      size={10}
+                    />
+                    <Text lineBreak="anywhere">
+                      {user.name
+                        ? `${user.name} (${user.address.slice(0, 6)}...${user.address.slice(-4)})`
+                        : user.address}
+                    </Text>
+                  </HStack>
+                </List.Item>
+              ))}
+            </List.Root>
+          </>
+        ) : (
+          <>
+            <Field label="送信量" alignItems="center" justifyContent="center">
+              <Input
+                p={2}
+                pb={4}
+                fontSize="60px"
+                size="2xl"
+                border="none"
+                borderBottom="2px solid"
+                borderRadius="0"
+                w="auto"
+                type="number"
+                textAlign="center"
+                min={0}
+                max={9999}
+                style={{
+                  WebkitAppearance: "none",
+                }}
+                value={amount}
+                onChange={(e) => setAmount(Number(e.target.value))}
+              />
+            </Field>
+
+            <Flex width="100%" flexDirection="column" alignItems="center">
+              <HStack columnGap={3} mb={4}>
+                <Box textAlign="center">
+                  <UserIcon
+                    size={10}
+                    userImageUrl={ipfs2https(me.identity?.text_records?.avatar)}
+                  />
+                  <Text fontSize="xs">{me.identity?.name}</Text>
+                </Box>
+                <VStack textAlign="center">
+                  <Text>{amount}</Text>
+                  <FaArrowRight size="20px" />
+                </VStack>
+                <Box>
                   <UserIcon
-                    userImageUrl={ipfs2https(user.text_records?.avatar)}
                     size={10}
+                    userImageUrl={ipfs2https(receiver.text_records?.avatar)}
                   />
-                  <Text lineBreak="anywhere">
-                    {user.name
-                      ? `${user.name} (${user.address.slice(0, 6)}...${user.address.slice(-4)})`
-                      : user.address}
+                  <Text fontSize="xs">
+                    {receiver.name || abbreviateAddress(receiver.address)}
                   </Text>
-                </HStack>
-              </List.Item>
-            ))}
-          </List.Root>
-        </>
-      ) : (
-        <>
-          <Field label="送信量" alignItems="center" justifyContent="center">
-            <Input
-              p={2}
-              pb={4}
-              fontSize="60px"
-              size="2xl"
-              border="none"
-              borderBottom="2px solid"
-              borderRadius="0"
-              w="auto"
-              type="number"
-              textAlign="center"
-              min={0}
-              max={9999}
-              style={{
-                WebkitAppearance: "none",
-              }}
-              value={amount}
-              onChange={(e) => setAmount(Number(e.target.value))}
-            />
-          </Field>
-
-          <Flex width="100%" flexDirection="column" alignItems="center">
-            <HStack columnGap={3} mb={4}>
-              <Box textAlign="center">
-                <UserIcon
-                  size={10}
-                  userImageUrl={ipfs2https(me.identity?.text_records?.avatar)}
-                />
-                <Text fontSize="xs">{me.identity?.name}</Text>
-              </Box>
-              <VStack textAlign="center">
-                <Text>{amount}</Text>
-                <FaArrowRight size="20px" />
-              </VStack>
-              <Box>
-                <UserIcon
-                  size={10}
-                  userImageUrl={ipfs2https(receiver.text_records?.avatar)}
-                />
-                <Text fontSize="xs">
-                  {receiver.name || abbreviateAddress(receiver.address)}
-                </Text>
-              </Box>
-            </HStack>
-            <BasicButton loading={isLoading} onClick={send} mb={5}>
-              送信
-            </BasicButton>
-          </Flex>
-        </>
-      )}
-    </Grid>
+                </Box>
+              </HStack>
+              <BasicButton loading={isLoading} onClick={send} mb={5}>
+                送信
+              </BasicButton>
+            </Flex>
+          </>
+        )}
+      </Grid>
+    </>
   );
 };
 
diff --git a/pkgs/frontend/hooks/useFractionToken.ts b/pkgs/frontend/hooks/useFractionToken.ts
index 8435712..d23a956 100644
--- a/pkgs/frontend/hooks/useFractionToken.ts
+++ b/pkgs/frontend/hooks/useFractionToken.ts
@@ -465,6 +465,7 @@ export const useTransferFractionToken = (hatId: bigint, wearer: Address) => {
       setIsLoading(true);
 
       let txHash: `0x${string}` | undefined = undefined;
+      let error: string | undefined = undefined;
       if (initialized) {
         try {
           txHash = await wallet.writeContract({
@@ -472,8 +473,8 @@ export const useTransferFractionToken = (hatId: bigint, wearer: Address) => {
             functionName: "safeTransferFrom",
             args: [wallet.account.address, to, tokenId, amount, "0x"],
           });
-        } catch (_) {
-          setIsLoading(false);
+        } catch {
+          error = "アシストクレジットの送信に失敗しました";
         } finally {
           setIsLoading(false);
         }
@@ -499,7 +500,6 @@ export const useTransferFractionToken = (hatId: bigint, wearer: Address) => {
           });
         } catch (error) {
           console.error(error);
-          setIsLoading(false);
         } finally {
           await publicClient.waitForTransactionReceipt({
             hash: txHash ?? "0x",
@@ -507,10 +507,11 @@ export const useTransferFractionToken = (hatId: bigint, wearer: Address) => {
           setIsLoading(false);
         }
       } else {
-        console.error("FractionToken is not initialized");
+        setIsLoading(false);
+        error = "この役割についてあなたはアシストクレジットの送信ができません";
       }
 
-      return txHash;
+      return { txHash, error };
     },
     [wallet, initialized, tokenId, hatId, wearer],
   );
diff --git a/pkgs/frontend/package.json b/pkgs/frontend/package.json
index 8b67bfd..5e25254 100644
--- a/pkgs/frontend/package.json
+++ b/pkgs/frontend/package.json
@@ -39,6 +39,8 @@
     "react-dom": "^18.3.1",
     "react-hook-form": "^7.54.2",
     "react-icons": "^5.4.0",
+    "react-toastify": "^11.0.3",
+    "remix-toast": "1.2.2",
     "viem": "^2.21.51"
   },
   "devDependencies": {

From 1a4010a32d94476e23966c2b1397e9a9cc795c9c Mon Sep 17 00:00:00 2001
From: aowheel <aoimakino2003@gmail.com>
Date: Tue, 21 Jan 2025 09:46:34 +0900
Subject: [PATCH 2/2] yarn.lock

---
 yarn.lock | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/yarn.lock b/yarn.lock
index 0d1e667..891de9a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9272,7 +9272,7 @@ clsx@^1.2.1:
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
   integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
 
-clsx@^2.0.0:
+clsx@^2.0.0, clsx@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
   integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
@@ -18402,6 +18402,13 @@ react-router@6.28.0:
   dependencies:
     "@remix-run/router" "1.21.0"
 
+react-toastify@^11.0.3:
+  version "11.0.3"
+  resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-11.0.3.tgz#1684de60baf745e761d3c608bb29581657e2fe01"
+  integrity sha512-cbPtHJPfc0sGqVwozBwaTrTu1ogB9+BLLjd4dDXd863qYLj7DGrQ2sg5RAChjFUB4yc3w8iXOtWcJqPK/6xqRQ==
+  dependencies:
+    clsx "^2.1.1"
+
 react@^18.0.0, react@^18.3.1:
   version "18.3.1"
   resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
@@ -18820,6 +18827,13 @@ remedial@^1.0.7:
   resolved "https://registry.yarnpkg.com/remedial/-/remedial-1.0.8.tgz#a5e4fd52a0e4956adbaf62da63a5a46a78c578a0"
   integrity sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==
 
+remix-toast@1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/remix-toast/-/remix-toast-1.2.2.tgz#13150cd082e70a2a70bea07cc22d72ce9e6cbf3a"
+  integrity sha512-GayR/ozpqCZWI9AK0+wzp/SaD3jIUnx7IBSL4Oioglp1gs3waGWlHhgBtG+IbLmCR9iUzzrux0HiZQdk8uwYVg==
+  dependencies:
+    zod "^3.22.3"
+
 remove-trailing-separator@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -22179,6 +22193,11 @@ zod@^3.21.4, zod@^3.22.4, zod@^3.23.8:
   resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
   integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
 
+zod@^3.22.3:
+  version "3.24.1"
+  resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee"
+  integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==
+
 zustand@^5.0.0:
   version "5.0.2"
   resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.2.tgz#f7595ada55a565f1fd6464f002a91e701ee0cfca"