浏览代码

Merge pull request #1940 from cprussin/add-legal-disclosure

feat(staking): add legal disclaimer on initial deposit
Connor Prussin 1 年之前
父节点
当前提交
e32ec93206

+ 158 - 10
apps/staking/src/components/AccountSummary/index.tsx

@@ -1,18 +1,28 @@
 import { InformationCircleIcon } from "@heroicons/react/24/outline";
 import { InformationCircleIcon } from "@heroicons/react/24/outline";
+import { useLocalStorageValue } from "@react-hookz/web";
 import Image from "next/image";
 import Image from "next/image";
-import { type ComponentProps, type ReactNode, useCallback } from "react";
+import {
+  type ComponentProps,
+  type FormEvent,
+  type ReactNode,
+  useCallback,
+  useState,
+} from "react";
 import {
 import {
   DialogTrigger,
   DialogTrigger,
+  Form,
   Button as ReactAriaButton,
   Button as ReactAriaButton,
 } from "react-aria-components";
 } from "react-aria-components";
 
 
 import background from "./background.png";
 import background from "./background.png";
 import { type States, StateType as ApiStateType } from "../../hooks/use-api";
 import { type States, StateType as ApiStateType } from "../../hooks/use-api";
 import { StateType, useAsync } from "../../hooks/use-async";
 import { StateType, useAsync } from "../../hooks/use-async";
-import { Button } from "../Button";
+import { Button, LinkButton } from "../Button";
+import { Checkbox } from "../Checkbox";
+import { Link } from "../Link";
 import { ModalDialog } from "../ModalDialog";
 import { ModalDialog } from "../ModalDialog";
 import { Tokens } from "../Tokens";
 import { Tokens } from "../Tokens";
-import { TransferButton } from "../TransferButton";
+import { TransferButton, TransferDialog } from "../TransferButton";
 
 
 type Props = {
 type Props = {
   api: States[ApiStateType.Loaded] | States[ApiStateType.LoadedNoStakeAccount];
   api: States[ApiStateType.Loaded] | States[ApiStateType.LoadedNoStakeAccount];
@@ -108,13 +118,7 @@ export const AccountSummary = ({
           </>
           </>
         )}
         )}
         <div className="mt-3 flex flex-row items-center gap-4 sm:mt-8">
         <div className="mt-3 flex flex-row items-center gap-4 sm:mt-8">
-          <TransferButton
-            actionDescription="Add funds to your balance"
-            actionName="Add Tokens"
-            max={walletAmount}
-            transfer={api.deposit}
-            enableWithZeroMax
-          />
+          <AddTokensButton walletAmount={walletAmount} api={api} />
           {availableToWithdraw === 0n ? (
           {availableToWithdraw === 0n ? (
             <DialogTrigger>
             <DialogTrigger>
               <Button variant="secondary" className="xl:hidden">
               <Button variant="secondary" className="xl:hidden">
@@ -354,3 +358,147 @@ const ClaimButton = ({ api, ...props }: ClaimButtonProps) => {
     </Button>
     </Button>
   );
   );
 };
 };
+
+type AddTokensButtonProps = {
+  walletAmount: bigint;
+  api: States[ApiStateType.Loaded] | States[ApiStateType.LoadedNoStakeAccount];
+};
+
+const AddTokensButton = ({ walletAmount, api }: AddTokensButtonProps) => {
+  const hasAcknowledgedLegal = useLocalStorageValue("has-acknowledged-legal");
+  const [transferOpen, setTransferOpen] = useState(false);
+  const openTransfer = useCallback(() => {
+    setTransferOpen(true);
+  }, [setTransferOpen]);
+  const acknowledgeLegal = useCallback(() => {
+    hasAcknowledgedLegal.set("true");
+    openTransfer();
+  }, [hasAcknowledgedLegal, openTransfer]);
+
+  return (
+    <>
+      {hasAcknowledgedLegal.value ? (
+        <Button onPress={openTransfer}>Add Tokens</Button>
+      ) : (
+        <DisclosureButton onAcknowledge={acknowledgeLegal} />
+      )}
+      <TransferDialog
+        title="Add tokens"
+        description="Add funds to your balance"
+        max={walletAmount}
+        transfer={api.deposit}
+        submitButtonText="Add tokens"
+        isOpen={transferOpen}
+        onOpenChange={setTransferOpen}
+      />
+    </>
+  );
+};
+
+type DisclosureButtonProps = {
+  onAcknowledge: () => void;
+};
+
+const DisclosureButton = ({ onAcknowledge }: DisclosureButtonProps) => {
+  const [understood, setUnderstood] = useState(false);
+  const [agreed, setAgreed] = useState(false);
+  const [open, setOpen] = useState(false);
+  const acknowledge = useCallback(
+    (e: FormEvent<HTMLFormElement>) => {
+      e.preventDefault();
+      setOpen(false);
+      setTimeout(onAcknowledge, 400);
+    },
+    [setOpen, onAcknowledge],
+  );
+
+  return (
+    <>
+      <DialogTrigger isOpen={open} onOpenChange={setOpen}>
+        <Button>Add Tokens</Button>
+        <ModalDialog title="Disclosure">
+          <Form onSubmit={acknowledge}>
+            <p className="max-w-prose text-sm opacity-60">
+              THE SERVICES WERE NOT DEVELOPED FOR, AND ARE NOT AVAILABLE TO
+              PERSONS OR ENTITIES WHO RESIDE IN, ARE LOCATED IN, ARE
+              INCORPORATED IN, OR HAVE A REGISTERED OFFICE OR PRINCIPAL PLACE OF
+              BUSINESS IN THE UNITED STATES OF AMERICA, THE UNITED KINGDOM OR
+              CANADA (COLLECTIVELY, “BLOCKED PERSONS”). MOREOVER, THE SERVICES
+              ARE NOT OFFERED TO PERSONS OR ENTITIES WHO RESIDE IN, ARE CITIZENS
+              OF, ARE LOCATED IN, ARE INCORPORATED IN, OR HAVE A REGISTERED
+              OFFICE OR PRINCIPAL PLACE OF BUSINESS IN ANY RESTRICTED
+              JURISDICTION OR COUNTRY SUBJECT TO ANY SANCTIONS OR RESTRICTIONS
+              PURSUANT TO ANY APPLICABLE LAW, INCLUDING THE CRIMEA REGION, CUBA,
+              IRAN, NORTH KOREA, SYRIA, MYANMAR (BURMA, DONETSK, LUHANSK, OR ANY
+              OTHER COUNTRY TO WHICH THE UNITED STATES, THE UNITED KINGDOM, THE
+              EUROPEAN UNION OR ANY OTHER JURISDICTIONS EMBARGOES GOODS OR
+              IMPOSES SIMILAR SANCTIONS, INCLUDING THOSE LISTED ON OUR SERVICES
+              (COLLECTIVELY, THE “RESTRICTED JURISDICTIONS” AND EACH A
+              “RESTRICTED JURISDICTION”) OR ANY PERSON OWNED, CONTROLLED,
+              LOCATED IN OR ORGANIZED UNDER THE LAWS OF ANY JURISDICTION UNDER
+              EMBARGO OR CONNECTED OR AFFILIATED WITH ANY SUCH PERSON
+              (COLLECTIVELY, “RESTRICTED PERSONS”). THE WEBSITE WAS NOT
+              SPECIFICALLY DEVELOPED FOR, AND IS NOT AIMED AT OR BEING ACTIVELY
+              MARKETED TO, PERSONS OR ENTITIES WHO RESIDE IN, ARE LOCATED IN,
+              ARE INCORPORATED IN, OR HAVE A REGISTERED OFFICE OR PRINCIPAL
+              PLACE OF BUSINESS IN THE EUROPEAN UNION. THERE ARE NO EXCEPTIONS.
+              IF YOU ARE A BLOCKED PERSON OR A RESTRICTED PERSON, THEN DO NOT
+              USE OR ATTEMPT TO USE THE SERVICES. USE OF ANY TECHNOLOGY OR
+              MECHANISM, SUCH AS A VIRTUAL PRIVATE NETWORK (“VPN”) TO CIRCUMVENT
+              THE RESTRICTIONS SET FORTH HEREIN IS PROHIBITED.
+            </p>
+            <Checkbox
+              className="my-4 block max-w-prose"
+              isSelected={understood}
+              onChange={setUnderstood}
+            >
+              I understand
+            </Checkbox>
+            <Checkbox
+              className="my-4 block max-w-prose"
+              isSelected={agreed}
+              onChange={setAgreed}
+            >
+              By checking the box and access the Services, you acknowledge and
+              agree to the terms and conditions of our{" "}
+              <Link
+                href="https://www.pyth.network/terms-of-use"
+                target="_blank"
+                className="underline"
+              >
+                Terms of Use
+              </Link>{" "}
+              and{" "}
+              <Link
+                href="https://www.pyth.network/privacy-policy"
+                target="_blank"
+                className="underline"
+              >
+                Privacy Policy
+              </Link>
+              .
+            </Checkbox>
+            <div className="mt-14 flex flex-col gap-8 sm:flex-row sm:justify-between">
+              <LinkButton
+                className="w-full sm:w-auto"
+                href="https://pyth.network/"
+                variant="secondary"
+                size="noshrink"
+              >
+                Exit
+              </LinkButton>
+              <Button
+                className="w-full sm:w-auto"
+                size="noshrink"
+                type="submit"
+                isDisabled={!understood || !agreed}
+              >
+                Confirm
+              </Button>
+            </div>
+          </Form>
+        </ModalDialog>
+      </DialogTrigger>
+    </>
+  );
+};

+ 20 - 0
apps/staking/src/components/Checkbox/index.tsx

@@ -0,0 +1,20 @@
+import { CheckIcon } from "@heroicons/react/24/outline";
+import clsx from "clsx";
+import type { ComponentProps, ReactNode } from "react";
+import { Checkbox as BaseCheckbox } from "react-aria-components";
+
+type Props = Omit<ComponentProps<typeof BaseCheckbox>, "children"> & {
+  children: ReactNode;
+};
+
+export const Checkbox = ({ className, children, ...props }: Props) => (
+  <BaseCheckbox
+    className={clsx("group flex cursor-pointer flex-row gap-2", className)}
+    {...props}
+  >
+    <div className="relative top-1 size-4 flex-none rounded border border-pythpurple-400 transition duration-100 group-data-[selected]:bg-pythpurple-600">
+      <CheckIcon className="absolute inset-0 stroke-2 opacity-0 transition duration-100 group-data-[selected]:opacity-100" />
+    </div>
+    <div>{children}</div>
+  </BaseCheckbox>
+);

+ 10 - 7
apps/staking/src/components/OracleIntegrityStaking/index.tsx

@@ -79,10 +79,11 @@ export const OracleIntegrityStaking = ({
 }: Props) => {
 }: Props) => {
   const self = useMemo(
   const self = useMemo(
     () =>
     () =>
-      api.type === ApiStateType.Loaded &&
-      publishers.find((publisher) =>
-        publisher.stakeAccount?.equals(api.account),
-      ),
+      api.type === ApiStateType.Loaded
+        ? publishers.find((publisher) =>
+            publisher.stakeAccount?.equals(api.account),
+          )
+        : undefined,
     [publishers, api],
     [publishers, api],
   );
   );
 
 
@@ -130,7 +131,7 @@ export const OracleIntegrityStaking = ({
       <div
       <div
         className={clsx(
         className={clsx(
           "relative -mx-4 overflow-hidden border-t border-neutral-600/50 pt-6 sm:-mx-8 lg:mt-10",
           "relative -mx-4 overflow-hidden border-t border-neutral-600/50 pt-6 sm:-mx-8 lg:mt-10",
-          { "mt-6": self === undefined },
+          { "mt-6 sm:mt-12": self === undefined },
         )}
         )}
       >
       >
         <PublisherList
         <PublisherList
@@ -622,8 +623,10 @@ const PublisherList = ({
 
 
   return (
   return (
     <div className="relative w-full overflow-x-auto">
     <div className="relative w-full overflow-x-auto">
-      <div className="sticky left-0 mb-4 flex flex-col gap-4 px-4 sm:px-10 sm:pb-4 sm:pt-6 md:flex-row md:items-center md:justify-between md:gap-12">
-        <h3 className="flex-none text-2xl font-light">{title}</h3>
+      <div className="sticky left-0 mb-4 flex flex-col gap-4 px-4 sm:px-10 sm:pb-4 sm:pt-6 md:flex-row md:justify-between md:gap-12 lg:items-center">
+        <h3 className="flex-none text-2xl font-light md:mt-1 lg:mt-0">
+          {title}
+        </h3>
 
 
         <div className="flex flex-none grow flex-col items-end gap-2 lg:flex-row-reverse lg:items-center lg:justify-start lg:gap-10 xl:gap-16">
         <div className="flex flex-none grow flex-col items-end gap-2 lg:flex-row-reverse lg:items-center lg:justify-start lg:gap-10 xl:gap-16">
           <SearchField
           <SearchField

+ 46 - 16
apps/staking/src/components/TransferButton/index.tsx

@@ -49,8 +49,6 @@ export const TransferButton = ({
   isDisabled,
   isDisabled,
   ...props
   ...props
 }: Props) => {
 }: Props) => {
-  const [closeDisabled, setCloseDisabled] = useState(false);
-
   return transfer === undefined ||
   return transfer === undefined ||
     isDisabled === true ||
     isDisabled === true ||
     (max === 0n && !enableWithZeroMax) ? (
     (max === 0n && !enableWithZeroMax) ? (
@@ -60,27 +58,59 @@ export const TransferButton = ({
   ) : (
   ) : (
     <DialogTrigger>
     <DialogTrigger>
       <Button {...props}>{actionName}</Button>
       <Button {...props}>{actionName}</Button>
-      <ModalDialog
+      <TransferDialog
         title={title ?? actionName}
         title={title ?? actionName}
-        closeDisabled={closeDisabled}
         description={actionDescription}
         description={actionDescription}
+        max={max}
+        transfer={transfer}
+        submitButtonText={submitButtonText ?? actionName}
       >
       >
-        {({ close }) => (
-          <DialogContents
-            max={max}
-            transfer={transfer}
-            setCloseDisabled={setCloseDisabled}
-            submitButtonText={submitButtonText ?? actionName}
-            close={close}
-          >
-            {children}
-          </DialogContents>
-        )}
-      </ModalDialog>
+        {children}
+      </TransferDialog>
     </DialogTrigger>
     </DialogTrigger>
   );
   );
 };
 };
 
 
+type TransferDialogProps = Omit<
+  ComponentProps<typeof ModalDialog>,
+  "children"
+> & {
+  max: bigint;
+  transfer: (amount: bigint) => Promise<void>;
+  submitButtonText: ReactNode;
+  children?:
+    | ((amount: Amount) => ReactNode | ReactNode[])
+    | ReactNode
+    | ReactNode[]
+    | undefined;
+};
+
+export const TransferDialog = ({
+  max,
+  transfer,
+  submitButtonText,
+  children,
+  ...props
+}: TransferDialogProps) => {
+  const [closeDisabled, setCloseDisabled] = useState(false);
+
+  return (
+    <ModalDialog closeDisabled={closeDisabled} {...props}>
+      {({ close }) => (
+        <DialogContents
+          max={max}
+          transfer={transfer}
+          setCloseDisabled={setCloseDisabled}
+          submitButtonText={submitButtonText}
+          close={close}
+        >
+          {children}
+        </DialogContents>
+      )}
+    </ModalDialog>
+  );
+};
+
 type DialogContentsProps = {
 type DialogContentsProps = {
   max: bigint;
   max: bigint;
   children: Props["children"];
   children: Props["children"];