diff --git a/.changeset/calm-coats-approve.md b/.changeset/calm-coats-approve.md new file mode 100644 index 00000000..c974548c --- /dev/null +++ b/.changeset/calm-coats-approve.md @@ -0,0 +1,7 @@ +--- +"@burnt-labs/abstraxion": major +"abstraxion-dashboard": minor +"demo-app": minor +--- + +Moved display logic to internal "useModal" hook. Consumers will need to change their strategy from a custom piece of state within their app to utilizing this new hook. The login flow will now be a single tab experience. diff --git a/.changeset/odd-planets-hammer.md b/.changeset/odd-planets-hammer.md new file mode 100644 index 00000000..39c17492 --- /dev/null +++ b/.changeset/odd-planets-hammer.md @@ -0,0 +1,6 @@ +--- +"abstraxion-dashboard": minor +"@burnt-labs/ui": minor +--- + +Added font files and small ui tweaks diff --git a/apps/abstraxion-dashboard/components/AbstraxionGrant/index.tsx b/apps/abstraxion-dashboard/components/AbstraxionGrant/index.tsx index 5eab11f7..3cf07a89 100644 --- a/apps/abstraxion-dashboard/components/AbstraxionGrant/index.tsx +++ b/apps/abstraxion-dashboard/components/AbstraxionGrant/index.tsx @@ -1,5 +1,5 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import Image from "next/image"; import { Button, Spinner } from "@burnt-labs/ui"; import { MsgGrant } from "cosmjs-types/cosmos/authz/v1beta1/tx"; @@ -11,6 +11,7 @@ import { useAbstraxionAccount, useAbstraxionSigningClient } from "@/hooks"; import burntAvatar from "@/public/burntAvatarCircle.png"; import { CheckIcon } from "../Icons"; import { EncodeObject } from "@cosmjs/proto-signing"; +import { redirect, useSearchParams } from "next/navigation"; interface AbstraxionGrantProps { contracts: string[]; @@ -23,10 +24,29 @@ export const AbstraxionGrant = ({ }: AbstraxionGrantProps) => { const { client } = useAbstraxionSigningClient(); const { data: account } = useAbstraxionAccount(); + const searchParams = useSearchParams(); const [inProgress, setInProgress] = useState(false); const [showSuccess, setShowSuccess] = useState(false); + useEffect(function redirectToDapp() { + if (showSuccess && searchParams.get("redirect_uri")) { + let redirectUri = new URLSearchParams(window.location.search).get( + "redirect_uri", + ); + let url: URL | null = null; + if (redirectUri) { + url = new URL(redirectUri); + let params = new URLSearchParams(url.search); + + params.append("granted", "true"); + url.search = params.toString(); + redirectUri = url.toString(); + window.location.href = redirectUri; + } + } + }); + const generateContractGrant = (granter: string) => { const timestampThreeMonthsFromNow = Math.floor( new Date(new Date().setMonth(new Date().getMonth() + 3)).getTime() / 1000, diff --git a/apps/demo-app/src/app/page.tsx b/apps/demo-app/src/app/page.tsx index e1772635..86cb9298 100644 --- a/apps/demo-app/src/app/page.tsx +++ b/apps/demo-app/src/app/page.tsx @@ -5,6 +5,7 @@ import { Abstraxion, useAbstraxionAccount, useAbstraxionSigningClient, + useModal, } from "@burnt-labs/abstraxion"; import { Button } from "@burnt-labs/ui"; import "@burnt-labs/ui/styles.css"; @@ -13,19 +14,23 @@ import { seatContractAddress } from "./layout"; type ExecuteResultOrUndefined = ExecuteResult | undefined; export default function Page(): JSX.Element { + // console.log("hello"); // Abstraxion hooks const { data: account } = useAbstraxionAccount(); const { client } = useAbstraxionSigningClient(); // General state hooks - const [isOpen, setIsOpen] = useState(false); + const [, setShowModal]: [ + boolean, + React.Dispatch>, + ] = useModal(); const [loading, setLoading] = useState(false); const [executeResult, setExecuteResult] = useState(undefined); const blockExplorerUrl = `https://explorer.burnt.com/xion-testnet-1/tx/${executeResult?.transactionHash}`; - function getTimestampInSeconds(date: Date | null) { + function getTimestampInSeconds(date: Date | null): number { if (!date) return 0; const d = new Date(date); return Math.floor(d.getTime() / 1000); @@ -36,7 +41,7 @@ export default function Page(): JSX.Element { const oneYearFromNow = new Date(); oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1); - async function claimSeat() { + async function claimSeat(): Promise { setLoading(true); const msg = { sales: { @@ -79,7 +84,7 @@ export default function Page(): JSX.Element { ) : null} { - setIsOpen(false); + setShowModal(false); }} /> {executeResult ? ( diff --git a/packages/abstraxion/.eslintrc.js b/packages/abstraxion/.eslintrc.js index cc219ae2..0f9d27e8 100644 --- a/packages/abstraxion/.eslintrc.js +++ b/packages/abstraxion/.eslintrc.js @@ -4,5 +4,6 @@ module.exports = { rules: { "no-nested-ternary": "off", "no-unnecessary-condition": "off", + "@typescript-eslint/no-floating-promises": "off", }, }; diff --git a/packages/abstraxion/interfaces/index.ts b/packages/abstraxion/interfaces/index.ts new file mode 100644 index 00000000..6cd577c8 --- /dev/null +++ b/packages/abstraxion/interfaces/index.ts @@ -0,0 +1,36 @@ +export interface GrantsResponse { + grants: Grant[]; + pagination: Pagination; +} + +export interface Grant { + granter: string; + grantee: string; + authorization: Authorization; + expiration: string; +} + +export interface Authorization { + "@type": string; + grants: GrantAuthorization[]; +} + +export interface GrantAuthorization { + contract: string; + limit: Limit; + filter: Filter; +} + +export interface Limit { + "@type": string; + remaining: string; +} + +export interface Filter { + "@type": string; +} + +export interface Pagination { + next_key: null | string; + total: string; +} diff --git a/packages/abstraxion/src/components/Abstraxion/index.tsx b/packages/abstraxion/src/components/Abstraxion/index.tsx index 725197c4..fe277f18 100644 --- a/packages/abstraxion/src/components/Abstraxion/index.tsx +++ b/packages/abstraxion/src/components/Abstraxion/index.tsx @@ -5,26 +5,32 @@ import { AbstraxionContext, AbstraxionContextProvider, } from "../AbstraxionContext"; -import { AbstraxionSignin } from "../AbstraxionSignin"; import { Loading } from "../Loading"; import { ErrorDisplay } from "../ErrorDisplay"; import { Connected } from "../Connected/Connected"; +import { AbstraxionSignin } from "../AbstraxionSignin"; export interface ModalProps { onClose: VoidFunction; - isOpen: boolean; } -export function Abstraxion({ - isOpen, - onClose, -}: ModalProps): JSX.Element | null { - const { abstraxionError, isConnecting, isConnected } = - useContext(AbstraxionContext); +export function Abstraxion({ onClose }: ModalProps): JSX.Element | null { + const { + abstraxionError, + isConnecting, + isConnected, + showModal, + setShowModal, + } = useContext(AbstraxionContext); useEffect(() => { const closeOnEscKey = (e: KeyboardEventInit): void => { - e.key === "Escape" ? onClose() : null; + e.key === "Escape" + ? () => { + onClose(); + setShowModal(false); + } + : null; }; document.addEventListener("keydown", closeOnEscKey); return () => { @@ -32,10 +38,10 @@ export function Abstraxion({ }; }, [onClose]); - if (!isOpen) return null; + if (!showModal) return null; return ( - + {abstraxionError ? ( diff --git a/packages/abstraxion/src/components/AbstraxionContext/index.tsx b/packages/abstraxion/src/components/AbstraxionContext/index.tsx index 0312210a..c4bccfea 100644 --- a/packages/abstraxion/src/components/AbstraxionContext/index.tsx +++ b/packages/abstraxion/src/components/AbstraxionContext/index.tsx @@ -1,5 +1,6 @@ -import { ReactNode, createContext, useState } from "react"; -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import type { ReactNode } from "react"; +import { useEffect, createContext, useState } from "react"; +import type { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; export interface AbstraxionContextProps { isConnected: boolean; @@ -11,7 +12,9 @@ export interface AbstraxionContextProps { abstraxionAccount: DirectSecp256k1HdWallet | undefined; setAbstraxionAccount: React.Dispatch; granterAddress: string; - setgranterAddress: React.Dispatch>; + showModal: boolean; + setShowModal: React.Dispatch>; + setGranterAddress: React.Dispatch>; contracts?: string[]; dashboardUrl?: string; } @@ -20,7 +23,7 @@ export const AbstraxionContext = createContext( {} as AbstraxionContextProps, ); -export const AbstraxionContextProvider = ({ +export function AbstraxionContextProvider({ children, contracts, dashboardUrl = "https://dashboard.burnt.com", @@ -28,14 +31,22 @@ export const AbstraxionContextProvider = ({ children: ReactNode; contracts?: string[]; dashboardUrl?: string; -}) => { +}): JSX.Element { const [abstraxionError, setAbstraxionError] = useState(""); const [isConnected, setIsConnected] = useState(false); const [isConnecting, setIsConnecting] = useState(false); + const [showModal, setShowModal] = useState(false); const [abstraxionAccount, setAbstraxionAccount] = useState< DirectSecp256k1HdWallet | undefined >(undefined); - const [granterAddress, setgranterAddress] = useState(""); + const [granterAddress, setGranterAddress] = useState(""); + + useEffect(() => { + const searchParams = new URLSearchParams(window.location.search); + if (searchParams.get("granted") === "true") { + setShowModal(true); + } + }, []); return ( ); -}; +} diff --git a/packages/abstraxion/src/components/AbstraxionSignin/index.tsx b/packages/abstraxion/src/components/AbstraxionSignin/index.tsx index 75f08698..803b8bf7 100644 --- a/packages/abstraxion/src/components/AbstraxionSignin/index.tsx +++ b/packages/abstraxion/src/components/AbstraxionSignin/index.tsx @@ -1,53 +1,55 @@ "use client"; import { useContext, useEffect, useRef, useState } from "react"; import { Button, ModalSection, BrowserIcon } from "@burnt-labs/ui"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; import { wait } from "@/utils/wait"; import { AbstraxionContext } from "../AbstraxionContext"; -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -type GrantsResponse = { +interface GrantsResponse { grants: Grant[]; pagination: Pagination; -}; +} -type Grant = { +interface Grant { granter: string; grantee: string; authorization: Authorization; expiration: string; -}; +} -type Authorization = { +interface Authorization { "@type": string; grants: GrantAuthorization[]; -}; +} -type GrantAuthorization = { +interface GrantAuthorization { contract: string; limit: Limit; filter: Filter; -}; +} -type Limit = { +interface Limit { "@type": string; remaining: string; -}; +} -type Filter = { +interface Filter { "@type": string; -}; +} -type Pagination = { +interface Pagination { next_key: null | string; total: string; -}; +} export function AbstraxionSignin(): JSX.Element { const { setIsConnecting, setIsConnected, setAbstraxionAccount, - setgranterAddress, + abstraxionAccount, + setGranterAddress, + granterAddress, contracts, dashboardUrl, } = useContext(AbstraxionContext); @@ -56,17 +58,22 @@ export function AbstraxionSignin(): JSX.Element { const [tempAccountAddress, setTempAccountAddress] = useState(""); function configuregranter(address: string) { - setgranterAddress(address); + setGranterAddress(address); localStorage.setItem("xion-authz-granter-account", address); } - function openDashboardTab(userAddress: string, contracts?: string[]): void { + function openDashboardTab( + userAddress: string, + grantContracts?: string[], + ): void { + const currentUrl = window.location.href; const urlParams = new URLSearchParams(); urlParams.set("grantee", userAddress); - // @ts-ignore - url encoding array - urlParams.set("contracts", contracts); - urlParams.toString(); - window.open(`${dashboardUrl}?${urlParams}`, "_blank"); + // @ts-expect-error - url encoding array + urlParams.set("contracts", grantContracts); + urlParams.set("redirect_uri", currentUrl); + const queryString = urlParams.toString(); // Convert URLSearchParams to string + window.location.href = `${dashboardUrl}?${queryString}`; } async function generateAndStoreTempAccount(): Promise { @@ -83,6 +90,7 @@ export function AbstraxionSignin(): JSX.Element { if (!address) { throw new Error("No keypair address"); } + setIsConnecting(true); const shouldContinue = true; while (shouldContinue) { @@ -95,8 +103,7 @@ export function AbstraxionSignin(): JSX.Element { }, ); const data = (await res.json()) as GrantsResponse; - if (data.grants?.length > 0) { - setIsConnecting(true); + if (data.grants.length > 0) { const granterAddresses = data.grants.map((grant) => grant.granter); const uniqueGranters = [...new Set(granterAddresses)]; if (uniqueGranters.length > 1) { @@ -104,6 +111,11 @@ export function AbstraxionSignin(): JSX.Element { } configuregranter(uniqueGranters[0]); + // Remove query parameter "granted" + const currentUrl = new URL(window.location.href); + currentUrl.searchParams.delete("granted"); + history.pushState({}, "", currentUrl.href); + break; } } catch (error) { @@ -125,14 +137,22 @@ export function AbstraxionSignin(): JSX.Element { } else { keypair = await generateAndStoreTempAccount(); } + const searchParams = new URLSearchParams(window.location.search); + const isGranted = searchParams.get("granted"); const accounts = await keypair.getAccounts(); const address = accounts[0].address; setTempAccountAddress(address); - openDashboardTab(address, contracts); - await pollForGrants(address); - setIsConnecting(false); - setIsConnected(true); - setAbstraxionAccount(keypair); + + if (!isGranted && !granterAddress) { + openDashboardTab(address, contracts); + } else if (isGranted && !granterAddress) { + await pollForGrants(address); + setIsConnecting(false); + setIsConnected(true); + setAbstraxionAccount(keypair); + } else { + setIsConnected(true); + } } catch (error) { console.log("Something went wrong: ", error); } @@ -158,7 +178,9 @@ export function AbstraxionSignin(): JSX.Element { + + + diff --git a/packages/abstraxion/src/hooks/index.ts b/packages/abstraxion/src/hooks/index.ts index 87d5dfda..d8606e3e 100644 --- a/packages/abstraxion/src/hooks/index.ts +++ b/packages/abstraxion/src/hooks/index.ts @@ -1,2 +1,3 @@ export { useAbstraxionAccount } from "./useAbstraxionAccount"; export { useAbstraxionSigningClient } from "./useAbstraxionSigningClient"; +export { useModal } from "./useModal"; diff --git a/packages/abstraxion/src/hooks/useAbstraxionAccount.ts b/packages/abstraxion/src/hooks/useAbstraxionAccount.ts index 9d914de0..49eab79f 100644 --- a/packages/abstraxion/src/hooks/useAbstraxionAccount.ts +++ b/packages/abstraxion/src/hooks/useAbstraxionAccount.ts @@ -1,30 +1,27 @@ import { useContext, useEffect } from "react"; -import { - AbstraxionContext, - AbstraxionContextProps, -} from "@/src/components/AbstraxionContext"; import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { AbstraxionContext } from "@/src/components/AbstraxionContext"; export interface AbstraxionAccount { bech32Address: string; } -export interface useAbstraxionAccountProps { +export interface UseAbstraxionAccountProps { data: AbstraxionAccount; isConnected: boolean; } -export const useAbstraxionAccount = (): useAbstraxionAccountProps => { +export const useAbstraxionAccount = (): UseAbstraxionAccountProps => { const { isConnected, granterAddress, abstraxionAccount, isConnecting, - setgranterAddress, + setGranterAddress, setAbstraxionAccount, setIsConnected, setIsConnecting, - } = useContext(AbstraxionContext) as AbstraxionContextProps; + } = useContext(AbstraxionContext); useEffect(() => { async function configureAccount() { @@ -40,14 +37,14 @@ export const useAbstraxionAccount = (): useAbstraxionAccountProps => { "xion-authz-granter-account", ); if (granterAccount) { - setgranterAddress(granterAccount); + setGranterAddress(granterAccount); setIsConnected(true); } } else { // Wipe granter even if it exists, clean context localStorage.removeItem("xion-authz-granter-account"); setAbstraxionAccount(undefined); - setgranterAddress(""); + setGranterAddress(""); } setIsConnecting(false); } @@ -61,6 +58,6 @@ export const useAbstraxionAccount = (): useAbstraxionAccountProps => { data: { bech32Address: granterAddress, }, - isConnected: isConnected, + isConnected, }; }; diff --git a/packages/abstraxion/src/hooks/useModal.ts b/packages/abstraxion/src/hooks/useModal.ts new file mode 100644 index 00000000..5b80e341 --- /dev/null +++ b/packages/abstraxion/src/hooks/useModal.ts @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import { AbstraxionContext } from "../components/AbstraxionContext"; + +export const useModal = (): [ + boolean, + React.Dispatch>, +] => { + const { showModal, setShowModal } = useContext(AbstraxionContext); + return [showModal, setShowModal]; +}; diff --git a/packages/abstraxion/src/index.ts b/packages/abstraxion/src/index.ts index 692f80b4..3f4abd70 100644 --- a/packages/abstraxion/src/index.ts +++ b/packages/abstraxion/src/index.ts @@ -2,4 +2,8 @@ import "@burnt-labs/ui/styles.css"; import "./styles.css"; export { Abstraxion, AbstraxionProvider } from "./components/Abstraxion"; -export { useAbstraxionAccount, useAbstraxionSigningClient } from "./hooks"; +export { + useAbstraxionAccount, + useAbstraxionSigningClient, + useModal, +} from "./hooks";