From d0469124c30697520b3a67580626f52a465ae7a7 Mon Sep 17 00:00:00 2001 From: Nicolas Henin Date: Fri, 3 Nov 2023 17:44:02 +0100 Subject: [PATCH] New Connection Flow (#27) --- src/components/App.tsx | 82 ++++++++------ src/components/Connection.tsx | 54 +++++----- src/components/Landing.tsx | 82 -------------- src/components/modals/NewVesting.tsx | 4 +- .../modals/SelectWalletExtension.tsx | 81 ++++++++++++++ src/components/modals/modal.scss | 11 +- src/components/vesting/About.tsx | 102 +----------------- src/components/vesting/Claimer.tsx | 33 ++---- src/components/vesting/Provider.tsx | 43 +++----- src/components/vesting/Utils.tsx | 2 +- src/styles/main.scss | 14 +-- 11 files changed, 200 insertions(+), 308 deletions(-) delete mode 100644 src/components/Landing.tsx create mode 100644 src/components/modals/SelectWalletExtension.tsx diff --git a/src/components/App.tsx b/src/components/App.tsx index 5d743a7..5b83051 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,12 +1,12 @@ -// App.tsx + import React, {useState} from 'react'; -import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom'; -import Landing from './Landing'; import ToastMessage from './ToastMessage'; -import About from './vesting/About'; -import YourTokenPlans from './vesting/Claimer'; -import CreatePlans from './vesting/Provider'; +import {About} from './vesting/About'; +import Claimer from './vesting/Claimer'; +import Provider from './vesting/Provider'; +import { Footer } from './Footer'; +import { ConnectionWallet } from './Connection'; type AppProps = { runtimeURL: string; @@ -14,9 +14,19 @@ type AppProps = { dAppId : string; } -const App: React.FC = ({runtimeURL,marloweScanURL,dAppId}) => { - const hasSelectedAWalletExtension = localStorage.getItem('walletProvider'); +type Page = "About" | "Token Provider's View" | "Claimer's View" +const aboutPage : Page = "About" +const isAboutPage = (page:Page) => page === aboutPage +const providerPage : Page = "Token Provider's View" +const isProviderPage = (page:Page) => page === providerPage +const claimerPage = "Claimer's View" +const isClaimerPage = (page:Page) => page === claimerPage + +const App: React.FC = ({runtimeURL,marloweScanURL,dAppId}) => { + const [isWaitingConfirmation, setWaitingConfirmation] = useState(false); + const [isConnected, setIsConnected] = useState(false); + const [currentPage, setCurrentPage] = useState("About"); const [toasts, setToasts] = useState([]); const setAndShowToast = (title: string, message: React.ReactNode) => { @@ -29,29 +39,39 @@ const App: React.FC = ({runtimeURL,marloweScanURL,dAppId}) => { } return ( - - - : } /> - : } /> - : } /> - : } /> - -
- {toasts.map(toast => ( - removeToast(toast.id)} - /> - ))} -
-
- - ); +
+
+
+

Token Plan Prototype

+

/ {currentPage}

+
+ setIsConnected(true)} onDisconnect={() => {setCurrentPage(aboutPage);setIsConnected(false)}} runtimeURL={runtimeURL} setAndShowToast={setAndShowToast} /> +
+
+ | + | +
+
+ {isAboutPage(currentPage)? + :(isProviderPage(currentPage)? + setWaitingConfirmation(true) } onConfirmation={() => setWaitingConfirmation(false)} runtimeURL={runtimeURL} marloweScanURL={marloweScanURL} dAppId={dAppId} setAndShowToast={setAndShowToast} /> + : + )} +
+
+ {toasts.map(toast => ( + removeToast(toast.id)} + /> + ))} +
+
); }; export default App; diff --git a/src/components/Connection.tsx b/src/components/Connection.tsx index 2f477d9..3a50101 100644 --- a/src/components/Connection.tsx +++ b/src/components/Connection.tsx @@ -4,39 +4,47 @@ import { BrowserRuntimeLifecycleOptions, mkRuntimeLifecycle } from '@marlowe.io/ import { mkRestClient } from '@marlowe.io/runtime-rest-client'; import { SupportedWalletName, getInstalledWalletExtensions } from '@marlowe.io/wallet/browser'; import React, { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; import { Image } from 'semantic-ui-react' +import { SelectWalletExtensionModal } from './modals/SelectWalletExtension'; type ConnectionWalletProps = { runtimeURL : string, + onConnect: () => void + onDisconnect: () => void setAndShowToast: (title:string, message:any, isDanger: boolean) => void }; -type DisconnectedProps = { - runtimeURL : string, - setAndShowToast: (title:string, message:any, isDanger: boolean) => void - }; - type ConnectedProps = { runtimeURL : string, + onDisconnect: () => void selectedWalletExtensionName : string setAndShowToast: (title:string, message:any, isDanger: boolean) => void }; -export const ConnectionWallet: React.FC = ({runtimeURL,setAndShowToast}) => { +export const ConnectionWallet: React.FC = ({runtimeURL,setAndShowToast,onConnect,onDisconnect}) => { + + const [showSelectWalletExtensionModal, setShowSelectWalletExtensionModal] = useState(false); const selectedWalletExtensionName = localStorage.getItem('walletProvider'); - if (!selectedWalletExtensionName) { return } - else { return } -} - -export const DisconnectedWallet: React.FC = ({runtimeURL}) => { - - return
Connection
- + if (!selectedWalletExtensionName) { + return (<>
+ +
+ onConnect()} + closeModal={() => setShowSelectWalletExtensionModal(false) } + />) + } + else { + return <> onDisconnect()} runtimeURL={runtimeURL} selectedWalletExtensionName={selectedWalletExtensionName} setAndShowToast={setAndShowToast}/> + } } -export const ConnectedWallet : React.FC = ({ runtimeURL,selectedWalletExtensionName,setAndShowToast }) => { - const navigate = useNavigate(); +export const ConnectedWallet : React.FC = ({ runtimeURL,selectedWalletExtensionName,setAndShowToast,onDisconnect }) => { const [runtimeLifecycle, setRuntimeLifecycle] = useState(); const [changeAddress, setChangeAddress] = useState('') const [isMainnet, setIsMainnet] = useState(false) @@ -60,8 +68,8 @@ export const ConnectedWallet : React.FC = ({ runtimeURL,selecte fetchData() // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedWalletExtensionName, navigate]); - + }, [selectedWalletExtensionName]); + const copyToClipboard = async () => { try { await navigator.clipboard.writeText(changeAddress); @@ -77,13 +85,7 @@ export const ConnectedWallet : React.FC = ({ runtimeURL,selecte const disconnectWallet = () => { localStorage.removeItem('walletProvider'); - - setAndShowToast( - 'Disconnected wallet', - Please, Connect a wallet to see your Token Plans., - false - ); - navigate('/'); + onDisconnect() } const adas = (Math.trunc(lovelaceBalance / 1_000_000)) diff --git a/src/components/Landing.tsx b/src/components/Landing.tsx deleted file mode 100644 index 57ce460..0000000 --- a/src/components/Landing.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { BroswerWalletExtension, getInstalledWalletExtensions } from '@marlowe.io/wallet/browser'; -import React, { } from 'react'; -import { useNavigate } from 'react-router-dom'; - - -type LandingProps = { - setAndShowToast: (title: string, message: any, isDanger: boolean) => void -}; - -const Landing: React.FC = ({ setAndShowToast }) => { - const navigate = useNavigate(); - const selectedAWalletExtension = localStorage.getItem('walletProvider'); - const installedWalletExtensions = getInstalledWalletExtensions() - if (selectedAWalletExtension) { navigate('/about') } - - async function connectWallet(walletName: string) { - localStorage.setItem('walletProvider', walletName); - setAndShowToast( - `Your ${walletName} wallet is connected `, - You can see Token Plans in which your {walletName} wallet is a participant Now!, - false - ); - navigate('/about'); - } - - function capitalizeFirstLetter(str: string): string { - return str.charAt(0).toUpperCase() + str.slice(1); - } - - function renderWallets(extension: BroswerWalletExtension) { - return (
-
connectWallet(extension.name)}> - Icon Before - {capitalizeFirstLetter(extension.name)} Wallet -
- Icon After - Cardano -
-
-
) - } - - return ( -
-
-
- -
-
-

Token Plans Prototype

-
-
-
-
-
-
-
-
-
Choose a wallet
-

Please, select a wallet to view your Token Plans.

-
-
- {installedWalletExtensions.map(extension => renderWallets(extension))} - -
-
-
-
-
-
-
- ); -}; - -export default Landing; diff --git a/src/components/modals/NewVesting.tsx b/src/components/modals/NewVesting.tsx index e47ad46..0e2f67d 100644 --- a/src/components/modals/NewVesting.tsx +++ b/src/components/modals/NewVesting.tsx @@ -149,8 +149,8 @@ const NewVestingScheduleModal: React.FC = ({ showM
New Vesting Schedule
-
diff --git a/src/components/modals/SelectWalletExtension.tsx b/src/components/modals/SelectWalletExtension.tsx new file mode 100644 index 0000000..a40c704 --- /dev/null +++ b/src/components/modals/SelectWalletExtension.tsx @@ -0,0 +1,81 @@ +import React, { } from 'react'; +import './modal.scss'; +import "react-datepicker/dist/react-datepicker.css"; +import { getInstalledWalletExtensions } from '@marlowe.io/wallet/browser'; + + +interface SelectWalletExtensionModalProps { + showModal: boolean; + closeModal: () => void; + onConnect: () => void +} + + +export const SelectWalletExtensionModal: React.FC = ({ showModal, closeModal,onConnect}) => { + + const installedWalletExtensions = getInstalledWalletExtensions() + + async function connectWallet(walletName: string) { + localStorage.setItem('walletProvider', walletName); + onConnect(); + closeModal() + } + + function capitalizeFirstLetter(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + return ( + <> +
+
+
+
+
+
+
+

Connnect to Wallet

+
+
+ +
+
+
+
+
+
+ {installedWalletExtensions.map(extension => +
connectWallet(extension.name)}> +
+ Icon Before + {capitalizeFirstLetter(extension.name)} +
+
+ {extension.supported? +

(Supports Marlowe Technology)

+ :

(May not support Marlowe Technology)

+ } +
+
+ Connect + +
+
+ )} +
+
+ + +
+
+
+ {showModal &&
} + + ); +}; + diff --git a/src/components/modals/modal.scss b/src/components/modals/modal.scss index 549fe9c..84d114d 100644 --- a/src/components/modals/modal.scss +++ b/src/components/modals/modal.scss @@ -1,9 +1,12 @@ .modal-title { - color: var(--button-contract-tab-default, #878FAA); - font-family: Outfit; - font-size: 1.625rem; + + font-family: Inter; + color:rgb(135, 143, 170); + font-size: 1.325rem; font-style: normal; - font-weight: 700; + margin: 0; + padding: 0; + font-weight: 400; line-height: normal; } .modal-header, .modal-footer { diff --git a/src/components/vesting/About.tsx b/src/components/vesting/About.tsx index a999bd8..268c314 100644 --- a/src/components/vesting/About.tsx +++ b/src/components/vesting/About.tsx @@ -1,101 +1,10 @@ -import React, { useEffect, useState } from 'react'; - -import { useNavigate } from 'react-router-dom'; - -import moment from 'moment'; - -import { BrowserRuntimeLifecycleOptions, mkRuntimeLifecycle } from "@marlowe.io/runtime-lifecycle/browser"; -import { Vesting } from "@marlowe.io/language-examples"; -import { mkRestClient } from "@marlowe.io/runtime-rest-client"; -import { AddressBech32, ContractId, Tags, unAddressBech32 } from '@marlowe.io/runtime-core'; -import { RuntimeLifecycle } from '@marlowe.io/runtime-lifecycle/api'; -import { ContractDetails } from '@marlowe.io/runtime-rest-client/contract/details'; -import HashLoader from 'react-spinners/HashLoader'; -import { Input } from '@marlowe.io/language-core-v1'; -import { ConnectionWallet } from '../Connection'; -import { SupportedWalletName } from '@marlowe.io/wallet/browser'; -import { Footer } from '../Footer'; - -const runtimeURL = `${process.env.MARLOWE_RUNTIME_WEB_URL}`; - -const dappId = "marlowe.examples.vesting.v0.0.4"; +import React from 'react'; type AboutProps = { - setAndShowToast: (title:string, message:any, isDanger: boolean) => void }; - -const About: React.FC = ({setAndShowToast}) => { - const navigate = useNavigate(); - const selectedAWalletExtension = localStorage.getItem('walletProvider'); - if (!selectedAWalletExtension) { navigate('/'); } - const [changeAddress, setChangeAddress] = useState('') - const truncatedAddress = changeAddress.slice(0,18); - - const copyToClipboard = async () => { - try { - await navigator.clipboard.writeText(changeAddress); - setAndShowToast( - 'Address copied to clipboard', - Copied {changeAddress} to clipboard, - false - ); - } catch (err) { - console.error('Failed to copy address: ', err); - } - }; - - const disconnectWallet = () => { - localStorage.removeItem('walletProvider'); - setChangeAddress(''); - setAndShowToast( - 'Disconnected wallet', - Please, Connect a wallet to see your Token Plans., - false - ); - navigate('/'); - } - - useEffect(() => { - const fetchData = async () => { - try { - const runtimeLifecycleParameters : BrowserRuntimeLifecycleOptions = { runtimeURL:runtimeURL, walletName:selectedAWalletExtension as SupportedWalletName} - const runtimeLifecycle = await mkRuntimeLifecycle(runtimeLifecycleParameters) - await runtimeLifecycle.wallet.getChangeAddress() - .then((changeAddress : AddressBech32) => setChangeAddress(unAddressBech32(changeAddress))) - - } catch (err : any) { - console.log("Error", err); - const error = JSON.parse(err); - const { message } = error; - setAndShowToast( - 'Failed Retrieving Payouts Infornation', - {message}, - true) - } - } - - fetchData() - const intervalId = setInterval(() => {fetchData()}, 10_000); // 5 seconds - // Clear the interval when the component is unmounted - return () => clearInterval(intervalId); - }, [selectedAWalletExtension, navigate, setAndShowToast]); - - +export const About: React.FC = () => { return ( -
-
-
-

Token Plan Prototype

-

/ About

-
- -
-
- | - | -
-

Overview

This Prototype is a Cardano/Marlowe DApp allowing you to create ₳ Token Plans over Cardano. ₳ Token Plans are created by a "Token Provider". The Provider will deposit a given ₳ amount with a time-based scheme @@ -135,12 +44,7 @@ const About: React.FC = ({setAndShowToast}) => {

The second iteration will allow you to create an infinite number of periods. The missing Marlowe feature to be provided at this DApp level is called Long Live Running Contract or Contract Merkleization. The capabilities are already available in the Runtime but not yet available in the Marlowe TS-SDK.

-

Enjoy and stay tuned for our next releases!

- +

Enjoy and stay tuned for our next releases!

-
-
); }; - -export default About; diff --git a/src/components/vesting/Claimer.tsx b/src/components/vesting/Claimer.tsx index a66332d..99c57bd 100644 --- a/src/components/vesting/Claimer.tsx +++ b/src/components/vesting/Claimer.tsx @@ -1,8 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; - - import { BrowserRuntimeLifecycleOptions, mkRuntimeLifecycle } from "@marlowe.io/runtime-lifecycle/browser"; import { Vesting } from "@marlowe.io/language-examples"; import { mkRestClient } from "@marlowe.io/runtime-rest-client"; @@ -13,8 +10,6 @@ import HashLoader from 'react-spinners/HashLoader'; import { Input } from '@marlowe.io/language-core-v1'; import { Contract } from './Models'; import { contractIdLink, cssOverrideSpinnerCentered, displayCloseCondition, formatADAs } from './Utils'; -import { ConnectionWallet } from '../Connection'; -import { Footer } from '../Footer'; import { SupportedWalletName } from '@marlowe.io/wallet/browser'; @@ -25,11 +20,10 @@ type YourTokenPlansProps = { setAndShowToast: (title:string, message:any, isDanger: boolean) => void }; -const YourTokenPlans: React.FC = ({runtimeURL,marloweScanURL,dAppId,setAndShowToast}) => { - const navigate = useNavigate(); +const Claimer: React.FC = ({runtimeURL,marloweScanURL,dAppId,setAndShowToast}) => { + const selectedAWalletExtension = localStorage.getItem('walletProvider'); - if (!selectedAWalletExtension) { navigate('/'); } - + const [runtimeLifecycle, setRuntimeLifecycle] = useState(); const [changeAddress, setChangeAddress] = useState('') @@ -138,7 +132,7 @@ const YourTokenPlans: React.FC = ({runtimeURL,marloweScanUR // Clear the interval when the component is unmounted return () => clearInterval(intervalId); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedAWalletExtension, contractsClosed,navigate]); + }, [selectedAWalletExtension, contractsClosed]); @@ -176,19 +170,7 @@ const YourTokenPlans: React.FC = ({runtimeURL,marloweScanUR } return ( -
-
-
-

Token Plan Prototype

-

/ Claimer's View

-
- -
-
- | - | -
-
+ <>
{(isFetchingFirstTime ?
@@ -300,10 +282,9 @@ const YourTokenPlans: React.FC = ({runtimeURL,marloweScanUR )}
-
-
+ ); }; -export default YourTokenPlans; \ No newline at end of file +export default Claimer; \ No newline at end of file diff --git a/src/components/vesting/Provider.tsx b/src/components/vesting/Provider.tsx index f95cb3a..d5703a4 100644 --- a/src/components/vesting/Provider.tsx +++ b/src/components/vesting/Provider.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; import moment from 'moment'; @@ -8,7 +7,7 @@ import NewVestingScheduleModal from '../modals/NewVesting'; import { BrowserRuntimeLifecycleOptions, mkRuntimeLifecycle } from "@marlowe.io/runtime-lifecycle/browser"; import { Vesting } from "@marlowe.io/language-examples"; import { mkRestClient } from "@marlowe.io/runtime-rest-client"; -import { AddressBech32, ContractId, Tags, addressBech32, contractId, unAddressBech32, unContractId } from '@marlowe.io/runtime-core'; +import { AddressBech32, ContractId, Tags, addressBech32, unAddressBech32, unContractId } from '@marlowe.io/runtime-core'; import { RuntimeLifecycle } from '@marlowe.io/runtime-lifecycle/api'; import { ContractDetails } from '@marlowe.io/runtime-rest-client/contract/details'; @@ -16,22 +15,21 @@ import HashLoader from 'react-spinners/HashLoader'; import { Address, Input } from '@marlowe.io/language-core-v1'; import { Contract } from './Models'; import { contractIdLink, cssOverrideSpinnerCentered, displayCloseCondition, formatADAs } from './Utils'; -import { ConnectionWallet } from '../Connection'; -import { Footer } from '../Footer'; import { SupportedWalletName } from '@marlowe.io/wallet/browser'; type CreatePlansProps = { runtimeURL : string, marloweScanURL : string, dAppId : string, + onWaitingConfirmation : () => void, + onConfirmation : () => void, setAndShowToast: (title:string, message:any, isDanger: boolean) => void }; -const CreatePlans: React.FC = ({runtimeURL,marloweScanURL,dAppId,setAndShowToast}) => { - const navigate = useNavigate(); +const Provider: React.FC = ({runtimeURL,marloweScanURL,dAppId,setAndShowToast,onWaitingConfirmation,onConfirmation}) => { + const selectedAWalletExtension = localStorage.getItem('walletProvider'); - if (!selectedAWalletExtension) { navigate('/'); } - + const [runtimeLifecycle, setRuntimeLifecycle] = useState(); const [changeAddress, setChangeAddress] = useState('') @@ -47,8 +45,6 @@ const CreatePlans: React.FC = ({runtimeURL,marloweScanURL,dApp const [showNewVestingScheduleModal, setShowNewVestingScheduleModal] = useState(false); - - useEffect(() => { const fetchData = async () => { if(isFetching) return; @@ -156,12 +152,11 @@ const CreatePlans: React.FC = ({runtimeURL,marloweScanURL,dApp // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedAWalletExtension,contractsClosed]); - - async function handleApplyInput(contractId : ContractId, actionName : string ,inputsToApply : Input[] | undefined) { try { if(inputsToApply && runtimeLifecycle) { setWaitingConfirmation(true) + onWaitingConfirmation() const txId = await runtimeLifecycle.contracts.applyInputs( contractId, { inputs: inputsToApply } @@ -180,9 +175,11 @@ const CreatePlans: React.FC = ({runtimeURL,marloweScanURL,dApp false ); setWaitingConfirmation(false) + onConfirmation() console.log(`Apply Input Confirmed on Cardano.`); }} catch (e) { setWaitingConfirmation(false) + onConfirmation() setAndShowToast( `${actionName} on Token Plan has Failed`, "Please Retry...", @@ -247,19 +244,7 @@ const CreatePlans: React.FC = ({runtimeURL,marloweScanURL,dApp return ( -
-
-
-

Token Plan Prototype

-

/ Token Provider's View

-
- -
-
- | - | -
-
+ <>
+ changeAddress={changeAddress} /> + ); }; -export default CreatePlans; \ No newline at end of file +export default Provider; \ No newline at end of file diff --git a/src/components/vesting/Utils.tsx b/src/components/vesting/Utils.tsx index d444aed..87402df 100644 --- a/src/components/vesting/Utils.tsx +++ b/src/components/vesting/Utils.tsx @@ -1,6 +1,6 @@ import { Vesting } from "@marlowe.io/language-examples"; import { ContractId, unContractId } from "@marlowe.io/runtime-core" -import React, { useEffect, useState } from 'react'; +import React, { } from 'react'; export function contractIdLink (marloweScanURL : string , contractId : ContractId) { return