Auto sign-in using laconic wallet and show link to Laconic store on low balance (#52)
Part of https://www.notion.so/Simplify-login-flow-in-deploy-laconic-com-190a6b22d47280a9924cc38f8cf4c891 - Send SIWE message to laconic wallet for auto signing - Remove WallletConnect - Remove log out functionality Co-authored-by: Shreerang Kale <shreerangkale@gmail.com> Co-authored-by: IshaVenikar <ishavenikar7@gmail.com> Reviewed-on: #52
This commit is contained in:
parent
a51765dae5
commit
6c79ed37fa
@ -15,7 +15,6 @@ VITE_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_github_clientid'
|
||||
VITE_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_pwa_templaterepo'
|
||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo'
|
||||
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_next_app_templaterepo'
|
||||
VITE_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_wallet_connect_id'
|
||||
VITE_LACONICD_CHAIN_ID = 'LACONIC_HOSTED_CONFIG_laconicd_chain_id'
|
||||
VITE_WALLET_IFRAME_URL = 'LACONIC_HOSTED_CONFIG_wallet_iframe_url'
|
||||
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
|
||||
|
@ -94,13 +94,4 @@ router.get('/session', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/logout', (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
return res.send({ success: false });
|
||||
}
|
||||
res.send({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
@ -133,7 +133,6 @@ record:
|
||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind
|
||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
|
||||
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||
LACONIC_HOSTED_CONFIG_wallet_iframe_url: https://wallet.laconic.com
|
||||
meta:
|
||||
|
@ -127,7 +127,6 @@ record:
|
||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind
|
||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
|
||||
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||
meta:
|
||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
||||
|
@ -5,8 +5,6 @@ VITE_GITHUB_PWA_TEMPLATE_REPO="snowball-tools/test-progressive-web-app"
|
||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO="snowball-tools/image-upload-pwa-example"
|
||||
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO="snowball-tools/starter.nextjs-react-tailwind"
|
||||
|
||||
VITE_WALLET_CONNECT_ID=
|
||||
|
||||
VITE_LIT_RELAY_API_KEY=
|
||||
|
||||
VITE_BUGSNAG_API_KEY=
|
||||
|
@ -41,13 +41,12 @@
|
||||
"@turnkey/http": "^2.10.0",
|
||||
"@turnkey/sdk-react": "^0.1.0",
|
||||
"@turnkey/webauthn-stamper": "^0.5.0",
|
||||
"@walletconnect/ethereum-provider": "^2.16.1",
|
||||
"@web3modal/siwe": "4.0.5",
|
||||
"@web3modal/wagmi": "4.0.5",
|
||||
"assert": "^2.1.0",
|
||||
"axios": "^1.6.7",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"ethers": "^5.6.2",
|
||||
"downshift": "^8.3.2",
|
||||
"framer-motion": "^11.0.8",
|
||||
"gql-client": "^1.0.0",
|
||||
@ -69,7 +68,6 @@
|
||||
"usehooks-ts": "^2.15.1",
|
||||
"uuid": "^9.0.1",
|
||||
"viem": "^2.7.11",
|
||||
"wagmi": "2.5.7",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -11,8 +11,8 @@ import ProjectSearchLayout from './layouts/ProjectSearch';
|
||||
import Index from './pages';
|
||||
import AuthPage from './pages/AuthPage';
|
||||
import { DashboardLayout } from './pages/org-slug/layout';
|
||||
import Web3Provider from 'context/Web3Provider';
|
||||
import { BASE_URL } from 'utils/constants';
|
||||
import BuyPrepaidService from './pages/BuyPrepaidService';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@ -50,6 +50,10 @@ const router = createBrowserRouter([
|
||||
path: '/login',
|
||||
element: <AuthPage />,
|
||||
},
|
||||
{
|
||||
path: '/buy-prepaid-service',
|
||||
element: <BuyPrepaidService />,
|
||||
},
|
||||
]);
|
||||
|
||||
function App() {
|
||||
@ -75,9 +79,7 @@ function App() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Web3Provider>
|
||||
<RouterProvider router={router} />
|
||||
</Web3Provider>
|
||||
<RouterProvider router={router} />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,60 +0,0 @@
|
||||
import {
|
||||
Select,
|
||||
Option,
|
||||
Spinner,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
const AccountsDropdown = ({
|
||||
accounts,
|
||||
isDataReceived,
|
||||
onAccountChange,
|
||||
}: {
|
||||
accounts: string[];
|
||||
isDataReceived: boolean;
|
||||
onAccountChange: (selectedAccount: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="p-6 bg-slate-100 dark:bg-overlay3 rounded-lg mb-6 shadow-md">
|
||||
{isDataReceived ? (
|
||||
!accounts.length ? (
|
||||
<div className="text-center">
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
No accounts found. Please visit{' '}
|
||||
<a
|
||||
href="https://store.laconic.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 underline dark:text-blue-400"
|
||||
>
|
||||
store.laconic.com
|
||||
</a>{' '}
|
||||
to create a wallet.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Select
|
||||
label="Select Account"
|
||||
defaultValue={accounts[0]}
|
||||
onChange={(value) => value && onAccountChange(value)}
|
||||
className="dark:bg-overlay2 dark:text-foreground"
|
||||
aria-label="Wallet Account Selector"
|
||||
>
|
||||
{accounts.map((account, index) => (
|
||||
<Option key={index} value={account}>
|
||||
{account}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-12">
|
||||
<Spinner className="h-6 w-6" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountsDropdown;
|
@ -7,12 +7,12 @@ import {
|
||||
VITE_WALLET_IFRAME_URL,
|
||||
} from 'utils/constants';
|
||||
|
||||
const IFrameModal = ({
|
||||
setAccounts,
|
||||
const ApproveTransactionModal = ({
|
||||
setAccount,
|
||||
setIsDataReceived,
|
||||
isVisible,
|
||||
}: {
|
||||
setAccounts: (accounts: string[]) => void;
|
||||
setAccount: (account: string) => void;
|
||||
setIsDataReceived: (isReceived: boolean) => void;
|
||||
isVisible: boolean;
|
||||
}) => {
|
||||
@ -20,10 +20,18 @@ const IFrameModal = ({
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== VITE_WALLET_IFRAME_URL) return;
|
||||
|
||||
setIsDataReceived(true);
|
||||
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
|
||||
setAccounts(event.data.data);
|
||||
} else if (event.data.type === 'ERROR') {
|
||||
setIsDataReceived(true);
|
||||
|
||||
if (event.data.data.length === 0) {
|
||||
console.error(`Accounts not present for chainId: ${VITE_LACONICD_CHAIN_ID}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setAccount(event.data.data[0].address);
|
||||
}
|
||||
|
||||
if (event.data.type === 'ERROR') {
|
||||
console.error('Error from wallet:', event.data.message);
|
||||
}
|
||||
};
|
||||
@ -85,4 +93,4 @@ const IFrameModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default IFrameModal;
|
||||
export default ApproveTransactionModal;
|
@ -0,0 +1,67 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Modal } from '@mui/material';
|
||||
|
||||
import { VITE_WALLET_IFRAME_URL } from 'utils/constants';
|
||||
import useCheckBalance from '../../../hooks/useCheckBalance';
|
||||
|
||||
const CHECK_BALANCE_INTERVAL = 5000;
|
||||
const IFRAME_ID = 'checkBalanceIframe';
|
||||
|
||||
const CheckBalanceIframe = ({
|
||||
onBalanceChange,
|
||||
isPollingEnabled,
|
||||
amount,
|
||||
}: {
|
||||
onBalanceChange: (value: boolean | undefined) => void;
|
||||
isPollingEnabled: boolean;
|
||||
amount: string;
|
||||
}) => {
|
||||
const { isBalanceSufficient, checkBalance } = useCheckBalance(
|
||||
amount,
|
||||
IFRAME_ID,
|
||||
);
|
||||
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoaded) {
|
||||
return;
|
||||
}
|
||||
checkBalance();
|
||||
}, [amount, checkBalance, isLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPollingEnabled || !isLoaded || isBalanceSufficient) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
checkBalance();
|
||||
}, CHECK_BALANCE_INTERVAL);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [isBalanceSufficient, isPollingEnabled, checkBalance, isLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
onBalanceChange(isBalanceSufficient);
|
||||
}, [isBalanceSufficient]);
|
||||
|
||||
return (
|
||||
<Modal open={false} disableEscapeKeyDown keepMounted>
|
||||
<iframe
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
id={IFRAME_ID}
|
||||
src={VITE_WALLET_IFRAME_URL}
|
||||
width="100%"
|
||||
height="100%"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
className="border rounded-md shadow-sm"
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckBalanceIframe;
|
@ -1,4 +1,4 @@
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
import { useCallback, useState, useEffect, useMemo } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { FormProvider, FieldValues } from 'react-hook-form';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
@ -8,6 +8,7 @@ import {
|
||||
AuctionParams,
|
||||
Deployer,
|
||||
} from 'gql-client';
|
||||
import { BigNumber } from 'ethers';
|
||||
|
||||
import { Select, MenuItem, FormControl, FormHelperText } from '@mui/material';
|
||||
|
||||
@ -20,14 +21,14 @@ import { Button } from '../../shared/Button';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
import IFrameModal from './IFrameModal';
|
||||
import ApproveTransactionModal from './ApproveTransactionModal';
|
||||
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
|
||||
import { EnvironmentVariablesFormValues } from 'types/types';
|
||||
import {
|
||||
VITE_LACONICD_CHAIN_ID,
|
||||
VITE_WALLET_IFRAME_URL,
|
||||
} from 'utils/constants';
|
||||
import AccountsDropdown from './AccountsDropdown';
|
||||
import CheckBalanceIframe from './CheckBalanceIframe';
|
||||
|
||||
type ConfigureDeploymentFormValues = {
|
||||
option: string;
|
||||
@ -46,12 +47,13 @@ const Configure = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [deployers, setDeployers] = useState<Deployer[]>([]);
|
||||
const [selectedAccount, setSelectedAccount] = useState<string>();
|
||||
const [accounts, setAccounts] = useState<string[]>([]);
|
||||
const [selectedDeployer, setSelectedDeployer] = useState<Deployer>();
|
||||
const [isPaymentLoading, setIsPaymentLoading] = useState(false);
|
||||
const [isPaymentDone, setIsPaymentDone] = useState(false);
|
||||
const [isFrameVisible, setIsFrameVisible] = useState(false);
|
||||
const [isAccountsDataReceived, setIsAccountsDataReceived] = useState(false);
|
||||
const [balanceMessage, setBalanceMessage] = useState<string>();
|
||||
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const templateId = searchParams.get('templateId');
|
||||
@ -81,10 +83,33 @@ const Configure = () => {
|
||||
});
|
||||
|
||||
const selectedOption = methods.watch('option');
|
||||
const selectedNumProviders = methods.watch('numProviders') ?? 1;
|
||||
const selectedMaxPrice = methods.watch('maxPrice') ?? DEFAULT_MAX_PRICE;
|
||||
|
||||
const isTabletView = useMediaQuery('(min-width: 720px)'); // md:
|
||||
const buttonSize = isTabletView ? { size: 'lg' as const } : {};
|
||||
|
||||
const amountToBePaid = useMemo(() => {
|
||||
let amount: string;
|
||||
|
||||
if (selectedOption === 'LRN') {
|
||||
amount = selectedDeployer?.minimumPayment?.toString() ?? '0';
|
||||
} else {
|
||||
if (!selectedNumProviders) {
|
||||
return '';
|
||||
}
|
||||
const bigMaxPrice = BigNumber.from(selectedMaxPrice);
|
||||
amount = bigMaxPrice.mul(selectedNumProviders).toString();
|
||||
}
|
||||
|
||||
return amount.replace(/\D/g, '');
|
||||
}, [
|
||||
selectedOption,
|
||||
selectedDeployer?.minimumPayment,
|
||||
selectedMaxPrice,
|
||||
selectedNumProviders,
|
||||
]);
|
||||
|
||||
const createProject = async (
|
||||
data: FieldValues,
|
||||
envVariables: AddEnvironmentVariableInput[],
|
||||
@ -186,7 +211,6 @@ const Configure = () => {
|
||||
(deployer) => deployer.deployerLrn === deployerLrn,
|
||||
);
|
||||
|
||||
let amount: string;
|
||||
let senderAddress: string;
|
||||
let txHash: string | null = null;
|
||||
if (createFormData.option === 'LRN' && !deployer?.minimumPayment) {
|
||||
@ -204,16 +228,6 @@ const Configure = () => {
|
||||
|
||||
senderAddress = selectedAccount;
|
||||
|
||||
if (createFormData.option === 'LRN') {
|
||||
amount = deployer?.minimumPayment!;
|
||||
} else {
|
||||
amount = (
|
||||
createFormData.numProviders * createFormData.maxPrice
|
||||
).toString();
|
||||
}
|
||||
|
||||
const amountToBePaid = amount.replace(/\D/g, '').toString();
|
||||
|
||||
txHash = await cosmosSendTokensHandler(senderAddress, amountToBePaid);
|
||||
|
||||
if (!txHash) {
|
||||
@ -303,7 +317,7 @@ const Configure = () => {
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
[client, createProject, dismiss, toast],
|
||||
[client, createProject, dismiss, toast, amountToBePaid],
|
||||
);
|
||||
|
||||
const fetchDeployers = useCallback(async () => {
|
||||
@ -311,10 +325,6 @@ const Configure = () => {
|
||||
setDeployers(res.deployers);
|
||||
}, [client]);
|
||||
|
||||
const onAccountChange = useCallback((account: string) => {
|
||||
setSelectedAccount(account);
|
||||
}, []);
|
||||
|
||||
const onDeployerChange = useCallback(
|
||||
(selectedLrn: string) => {
|
||||
const deployer = deployers.find((d) => d.deployerLrn === selectedLrn);
|
||||
@ -340,12 +350,20 @@ const Configure = () => {
|
||||
await requestTx(senderAddress, snowballAddress, amount);
|
||||
|
||||
const txHash = await new Promise<string | null>((resolve, reject) => {
|
||||
// Call cleanup method only if appropriate event type is recieved
|
||||
const cleanup = () => {
|
||||
setIsFrameVisible(false);
|
||||
window.removeEventListener('message', handleTxStatus);
|
||||
};
|
||||
|
||||
const handleTxStatus = async (event: MessageEvent) => {
|
||||
if (event.origin !== VITE_WALLET_IFRAME_URL) return;
|
||||
|
||||
if (event.data.type === 'TRANSACTION_RESPONSE') {
|
||||
const txResponse = event.data.data;
|
||||
resolve(txResponse);
|
||||
|
||||
cleanup();
|
||||
} else if (event.data.type === 'ERROR') {
|
||||
console.error('Error from wallet:', event.data.message);
|
||||
reject(new Error('Transaction failed'));
|
||||
@ -355,10 +373,9 @@ const Configure = () => {
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}
|
||||
setIsFrameVisible(false);
|
||||
|
||||
window.removeEventListener('message', handleTxStatus);
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleTxStatus);
|
||||
@ -418,6 +435,12 @@ const Configure = () => {
|
||||
fetchDeployers();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isBalanceSufficient) {
|
||||
setBalanceMessage(undefined);
|
||||
}
|
||||
}, [isBalanceSufficient]);
|
||||
|
||||
return (
|
||||
<div className="space-y-7 px-4 py-6">
|
||||
<div className="flex justify-between mb-6">
|
||||
@ -579,49 +602,81 @@ const Configure = () => {
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<AccountsDropdown
|
||||
accounts={accounts}
|
||||
onAccountChange={onAccountChange}
|
||||
isDataReceived={isAccountsDataReceived}
|
||||
/>
|
||||
{accounts.length > 0 && (
|
||||
<div>
|
||||
<Button
|
||||
{...buttonSize}
|
||||
type="submit"
|
||||
shape="default"
|
||||
disabled={
|
||||
isLoading || isPaymentLoading || !selectedAccount
|
||||
}
|
||||
rightIcon={
|
||||
isLoading || isPaymentLoading ? (
|
||||
<LoadingIcon className="animate-spin" />
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
{...buttonSize}
|
||||
type="submit"
|
||||
shape="default"
|
||||
disabled={
|
||||
isLoading ||
|
||||
isPaymentLoading ||
|
||||
!selectedAccount ||
|
||||
!isBalanceSufficient
|
||||
}
|
||||
rightIcon={
|
||||
isLoading || isPaymentLoading ? (
|
||||
<LoadingIcon className="animate-spin" />
|
||||
) : (
|
||||
<ArrowRightCircleFilledIcon />
|
||||
)
|
||||
}
|
||||
>
|
||||
{!isPaymentDone
|
||||
? isPaymentLoading
|
||||
? 'Transaction Requested'
|
||||
: 'Pay and Deploy'
|
||||
: isLoading
|
||||
? 'Deploying'
|
||||
: 'Deploy'}
|
||||
</Button>
|
||||
{isAccountsDataReceived && isBalanceSufficient !== undefined ? (
|
||||
!selectedAccount || !isBalanceSufficient ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
{...buttonSize}
|
||||
shape="default"
|
||||
onClick={(e: any) => {
|
||||
e.preventDefault();
|
||||
setBalanceMessage('Waiting for payment');
|
||||
window.open(
|
||||
'https://store.laconic.com',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
}}
|
||||
>
|
||||
Buy prepaid service
|
||||
</Button>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
{balanceMessage !== undefined ? (
|
||||
<div className="flex items-center gap-2 text-white">
|
||||
<LoadingIcon className="animate-spin w-5 h-5" />
|
||||
<p>{balanceMessage}</p>
|
||||
</div>
|
||||
) : !selectedAccount ? (
|
||||
'No accounts found. Create a wallet.'
|
||||
) : (
|
||||
<ArrowRightCircleFilledIcon />
|
||||
)
|
||||
}
|
||||
>
|
||||
{!isPaymentDone
|
||||
? isPaymentLoading
|
||||
? 'Transaction Requested'
|
||||
: 'Pay and Deploy'
|
||||
: isLoading
|
||||
? 'Deploying'
|
||||
: 'Deploy'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
'Insufficient funds.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
) : null
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
<IFrameModal
|
||||
setAccounts={setAccounts}
|
||||
<ApproveTransactionModal
|
||||
setAccount={setSelectedAccount}
|
||||
setIsDataReceived={setIsAccountsDataReceived}
|
||||
isVisible={isFrameVisible}
|
||||
/>
|
||||
<CheckBalanceIframe
|
||||
onBalanceChange={setIsBalanceSufficient}
|
||||
amount={amountToBePaid}
|
||||
isPollingEnabled={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,14 +1,11 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { User } from 'gql-client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useDisconnect } from 'wagmi';
|
||||
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import {
|
||||
GlobeIcon,
|
||||
LifeBuoyIcon,
|
||||
LogoutIcon,
|
||||
QuestionMarkRoundIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Tabs } from 'components/shared/Tabs';
|
||||
@ -16,10 +13,9 @@ import { Logo } from 'components/Logo';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { formatAddress } from 'utils/format';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { cn } from 'utils/classnames';
|
||||
import { useMediaQuery } from 'usehooks-ts';
|
||||
import { BASE_URL } from 'utils/constants';
|
||||
import { SHOPIFY_APP_URL } from '../../../constants';
|
||||
|
||||
interface SidebarProps {
|
||||
mobileOpen?: boolean;
|
||||
@ -27,12 +23,10 @@ interface SidebarProps {
|
||||
|
||||
export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
||||
const { orgSlug } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const client = useGQLClient();
|
||||
const isDesktop = useMediaQuery('(min-width: 960px)');
|
||||
|
||||
const [user, setUser] = useState<User>();
|
||||
const { disconnect } = useDisconnect();
|
||||
|
||||
const fetchUser = useCallback(async () => {
|
||||
const { user } = await client.getUser();
|
||||
@ -43,16 +37,6 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
||||
fetchUser();
|
||||
}, []);
|
||||
|
||||
const handleLogOut = useCallback(async () => {
|
||||
await fetch(`${BASE_URL}/auth/logout`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
localStorage.clear();
|
||||
disconnect();
|
||||
navigate('/login');
|
||||
}, [disconnect, navigate]);
|
||||
|
||||
return (
|
||||
<motion.nav
|
||||
initial={{ x: -320 }}
|
||||
@ -82,19 +66,10 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
||||
<Tabs defaultValue="Projects" orientation="vertical">
|
||||
{/* // TODO: use proper link buttons */}
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger
|
||||
icon={<GlobeIcon />}
|
||||
value=""
|
||||
className="hidden lg:flex"
|
||||
>
|
||||
<a className="cursor-pointer font-mono" onClick={handleLogOut}>
|
||||
LOG OUT
|
||||
</a>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger icon={<QuestionMarkRoundIcon />} value="">
|
||||
<a
|
||||
className="cursor-pointer font-mono"
|
||||
href="https://store.laconic.com/pages/instruction-faq"
|
||||
href={`${SHOPIFY_APP_URL}/pages/instruction-faq`}
|
||||
>
|
||||
DOCUMENTATION
|
||||
</a>
|
||||
@ -125,14 +100,6 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
iconOnly
|
||||
variant="ghost"
|
||||
className="text-elements-low-em"
|
||||
onClick={handleLogOut}
|
||||
>
|
||||
<LogoutIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</motion.nav>
|
||||
);
|
||||
|
@ -0,0 +1,154 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { generateNonce, SiweMessage } from 'siwe';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Box, Modal } from '@mui/material';
|
||||
|
||||
import { BASE_URL, VITE_WALLET_IFRAME_URL } from 'utils/constants';
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
const AutoSignInIFrameModal = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [accountAddress, setAccountAddress] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
const handleSignInResponse = async (event: MessageEvent) => {
|
||||
if (event.origin !== VITE_WALLET_IFRAME_URL) return;
|
||||
|
||||
if (event.data.type === 'SIGN_IN_RESPONSE') {
|
||||
try {
|
||||
const { success } = (
|
||||
await axiosInstance.post('/auth/validate', {
|
||||
message: event.data.data.message,
|
||||
signature: event.data.data.signature,
|
||||
})
|
||||
).data;
|
||||
|
||||
if (success === true) {
|
||||
navigate('/');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error signing in:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleSignInResponse);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', handleSignInResponse);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const initiateAutoSignIn = async () => {
|
||||
if (!accountAddress) return;
|
||||
|
||||
const iframe = document.getElementById(
|
||||
'autoSignInFrame',
|
||||
) as HTMLIFrameElement;
|
||||
|
||||
if (!iframe.contentWindow) {
|
||||
console.error('Iframe not found or not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
const message = new SiweMessage({
|
||||
version: '1',
|
||||
domain: window.location.host,
|
||||
uri: window.location.origin,
|
||||
chainId: 1,
|
||||
address: accountAddress,
|
||||
nonce: generateNonce(),
|
||||
// Human-readable ASCII assertion that the user will sign, and it must not contain `\n`.
|
||||
statement: 'Sign in With Ethereum.',
|
||||
}).prepareMessage();
|
||||
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: 'AUTO_SIGN_IN',
|
||||
chainId: '1',
|
||||
message,
|
||||
},
|
||||
VITE_WALLET_IFRAME_URL,
|
||||
);
|
||||
};
|
||||
|
||||
initiateAutoSignIn();
|
||||
}, [accountAddress]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleAccountsDataResponse = async (event: MessageEvent) => {
|
||||
if (event.origin !== VITE_WALLET_IFRAME_URL) return;
|
||||
|
||||
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
|
||||
setAccountAddress(event.data.data[0].address);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleAccountsDataResponse);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', handleAccountsDataResponse);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getAddressFromWallet = useCallback(() => {
|
||||
const iframe = document.getElementById(
|
||||
'autoSignInFrame',
|
||||
) as HTMLIFrameElement;
|
||||
|
||||
if (!iframe.contentWindow) {
|
||||
console.error('Iframe not found or not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: 'REQUEST_CREATE_OR_GET_ACCOUNTS',
|
||||
chainId: '1',
|
||||
},
|
||||
VITE_WALLET_IFRAME_URL,
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal open={true} disableEscapeKeyDown keepMounted>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '90%',
|
||||
maxWidth: '1200px',
|
||||
height: '600px',
|
||||
maxHeight: '80vh',
|
||||
overflow: 'auto',
|
||||
outline: 'none',
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
onLoad={getAddressFromWallet}
|
||||
id="autoSignInFrame"
|
||||
src={`${VITE_WALLET_IFRAME_URL}/auto-sign-in`}
|
||||
width="100%"
|
||||
height="100%"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
></iframe>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoSignInIFrameModal;
|
@ -1,3 +1,5 @@
|
||||
export const SHORT_COMMIT_HASH_LENGTH = 8;
|
||||
|
||||
export const SERVER_GQL_PATH = 'graphql';
|
||||
|
||||
export const SHOPIFY_APP_URL = 'https://store.laconic.com';
|
||||
|
@ -1,116 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import assert from 'assert';
|
||||
import { SiweMessage, generateNonce } from 'siwe';
|
||||
import { WagmiProvider } from 'wagmi';
|
||||
import { mainnet } from 'wagmi/chains';
|
||||
import axios from 'axios';
|
||||
|
||||
import { createWeb3Modal } from '@web3modal/wagmi/react';
|
||||
import { defaultWagmiConfig } from '@web3modal/wagmi/react/config';
|
||||
import { createSIWEConfig } from '@web3modal/siwe';
|
||||
import type {
|
||||
SIWECreateMessageArgs,
|
||||
SIWEVerifyMessageArgs,
|
||||
} from '@web3modal/core';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { VITE_WALLET_CONNECT_ID, BASE_URL } from 'utils/constants';
|
||||
|
||||
if (!VITE_WALLET_CONNECT_ID) {
|
||||
throw new Error('Error: VITE_WALLET_CONNECT_ID env config is not set');
|
||||
}
|
||||
assert(BASE_URL, 'VITE_SERVER_URL is not set in env');
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
const metadata = {
|
||||
name: 'Deploy App Auth',
|
||||
description: '',
|
||||
url: window.location.origin,
|
||||
icons: ['https://avatars.githubusercontent.com/u/37784886'],
|
||||
};
|
||||
const chains = [mainnet] as const;
|
||||
const config = defaultWagmiConfig({
|
||||
chains,
|
||||
projectId: VITE_WALLET_CONNECT_ID,
|
||||
metadata,
|
||||
});
|
||||
const siweConfig = createSIWEConfig({
|
||||
createMessage: ({ nonce, address, chainId }: SIWECreateMessageArgs) =>
|
||||
new SiweMessage({
|
||||
version: '1',
|
||||
domain: window.location.host,
|
||||
uri: window.location.origin,
|
||||
address,
|
||||
chainId,
|
||||
nonce,
|
||||
// Human-readable ASCII assertion that the user will sign, and it must not contain `\n`.
|
||||
statement: 'Sign in With Ethereum.',
|
||||
}).prepareMessage(),
|
||||
getNonce: async () => {
|
||||
return generateNonce();
|
||||
},
|
||||
getSession: async () => {
|
||||
try {
|
||||
const session = (await axiosInstance.get('/auth/session')).data;
|
||||
const { address, chainId } = session;
|
||||
return { address, chainId };
|
||||
} catch (err) {
|
||||
if (window.location.pathname !== '/login') {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
throw new Error('Failed to get session!');
|
||||
}
|
||||
},
|
||||
verifyMessage: async ({ message, signature }: SIWEVerifyMessageArgs) => {
|
||||
try {
|
||||
const { success } = (
|
||||
await axiosInstance.post('/auth/validate', {
|
||||
message,
|
||||
signature,
|
||||
})
|
||||
).data;
|
||||
return success;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
signOut: async () => {
|
||||
try {
|
||||
const { success } = (await axiosInstance.post('/auth/logout')).data;
|
||||
return success;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
onSignOut: () => {
|
||||
window.location.href = '/login';
|
||||
},
|
||||
onSignIn: () => {
|
||||
window.location.href = '/';
|
||||
},
|
||||
});
|
||||
|
||||
createWeb3Modal({
|
||||
siweConfig,
|
||||
wagmiConfig: config,
|
||||
projectId: VITE_WALLET_CONNECT_ID,
|
||||
});
|
||||
export default function Web3ModalProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<WagmiProvider config={config}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</WagmiProvider>
|
||||
);
|
||||
}
|
44
packages/frontend/src/hooks/useCheckBalance.tsx
Normal file
44
packages/frontend/src/hooks/useCheckBalance.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import { VITE_LACONICD_CHAIN_ID } from 'utils/constants';
|
||||
|
||||
const useCheckBalance = (amount: string, iframeId: string) => {
|
||||
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>();
|
||||
|
||||
const checkBalance = useCallback(() => {
|
||||
const iframe = document.getElementById(iframeId) as HTMLIFrameElement;
|
||||
|
||||
if (!iframe || !iframe.contentWindow) {
|
||||
console.error(`Iframe with ID "${iframeId}" not found or not loaded`);
|
||||
return;
|
||||
}
|
||||
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: 'CHECK_BALANCE',
|
||||
chainId: VITE_LACONICD_CHAIN_ID,
|
||||
amount,
|
||||
},
|
||||
import.meta.env.VITE_WALLET_IFRAME_URL
|
||||
);
|
||||
}, [iframeId, amount]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== import.meta.env.VITE_WALLET_IFRAME_URL) return;
|
||||
|
||||
if (event.data.type !== 'IS_SUFFICIENT') return;
|
||||
|
||||
setIsBalanceSufficient(event.data.data);
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
return () => {
|
||||
window.removeEventListener('message', handleMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { isBalanceSufficient, checkBalance };
|
||||
};
|
||||
|
||||
export default useCheckBalance;
|
@ -14,7 +14,6 @@ import { SERVER_GQL_PATH } from './constants';
|
||||
import { Toaster } from 'components/shared/Toast';
|
||||
import { LogErrorBoundary } from 'utils/log-error';
|
||||
import { BASE_URL } from 'utils/constants';
|
||||
import Web3ModalProvider from './context/Web3Provider';
|
||||
import './index.css';
|
||||
|
||||
console.log(`v-0.0.9`);
|
||||
@ -32,12 +31,10 @@ root.render(
|
||||
<LogErrorBoundary>
|
||||
<React.StrictMode>
|
||||
<ThemeProvider>
|
||||
<Web3ModalProvider>
|
||||
<GQLClientProvider client={gqlClient}>
|
||||
<App />
|
||||
<Toaster />
|
||||
</GQLClientProvider>
|
||||
</Web3ModalProvider>
|
||||
<GQLClientProvider client={gqlClient}>
|
||||
<App />
|
||||
<Toaster />
|
||||
</GQLClientProvider>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
</LogErrorBoundary>,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Login } from './auth/Login';
|
||||
import AutoSignInIFrameModal from 'components/shared/auth/AutoSignInIFrameModal';
|
||||
|
||||
const AuthPage = () => {
|
||||
return (
|
||||
@ -13,9 +13,7 @@ const AuthPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="pb-12 relative z-10 flex-1 flex-center">
|
||||
<div className="max-w-[520px] w-full dark:bg-overlay bg-white rounded-xl shadow">
|
||||
<Login />
|
||||
</div>
|
||||
<AutoSignInIFrameModal />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
38
packages/frontend/src/pages/BuyPrepaidService.tsx
Normal file
38
packages/frontend/src/pages/BuyPrepaidService.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useMediaQuery } from 'usehooks-ts';
|
||||
|
||||
import { Button } from 'components/shared';
|
||||
import CheckBalanceIframe from 'components/projects/create/CheckBalanceIframe';
|
||||
import { SHOPIFY_APP_URL } from '../constants';
|
||||
|
||||
const BuyPrepaidService = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>();
|
||||
|
||||
const isTabletView = useMediaQuery('(min-width: 720px)'); // md:
|
||||
const buttonSize = isTabletView ? { size: 'lg' as const } : {};
|
||||
|
||||
useEffect(() => {
|
||||
if (isBalanceSufficient === true) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [isBalanceSufficient]);
|
||||
|
||||
return (
|
||||
<div className="dark:bg-background flex flex-col min-h-screen">
|
||||
<div className="pb-12 relative z-10 flex-1 flex-center">
|
||||
<Button {...buttonSize} shape={'default'}>
|
||||
<a href={SHOPIFY_APP_URL} target="_blank">
|
||||
Buy prepaid service
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<CheckBalanceIframe onBalanceChange={setIsBalanceSufficient} isPollingEnabled={true} amount='1'/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BuyPrepaidService;
|
@ -9,12 +9,6 @@ export const Login = () => {
|
||||
</div>
|
||||
</div>
|
||||
<WavyBorder className="self-stretch" variant="stroke" />
|
||||
|
||||
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
|
||||
<div className="self-stretch flex-col justify-center items-center gap-3 flex">
|
||||
<w3m-button />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,16 +1,20 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { ProjectCard } from 'components/projects/ProjectCard';
|
||||
import { Heading, Badge, Button } from 'components/shared';
|
||||
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { Project } from 'gql-client';
|
||||
import CheckBalanceIframe from 'components/projects/create/CheckBalanceIframe';
|
||||
|
||||
const Projects = () => {
|
||||
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>();
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const client = useGQLClient();
|
||||
const { orgSlug } = useParams();
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
|
||||
const fetchProjects = useCallback(async () => {
|
||||
const { projectsInOrganization } = await client.getProjectsInOrganization(
|
||||
@ -23,6 +27,12 @@ const Projects = () => {
|
||||
fetchProjects();
|
||||
}, [orgSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isBalanceSufficient === false) {
|
||||
navigate('/buy-prepaid-service');
|
||||
}
|
||||
}, [isBalanceSufficient]);
|
||||
|
||||
return (
|
||||
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
|
||||
{/* Header */}
|
||||
@ -49,6 +59,8 @@ const Projects = () => {
|
||||
return <ProjectCard project={project} key={key} />;
|
||||
})}
|
||||
</div>
|
||||
|
||||
<CheckBalanceIframe onBalanceChange={setIsBalanceSufficient} isPollingEnabled={false} amount='1' />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@ -8,7 +8,6 @@ export const VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = import.meta.env
|
||||
export const VITE_GITHUB_NEXT_APP_TEMPLATE_REPO = import.meta.env
|
||||
.VITE_GITHUB_NEXT_APP_TEMPLATE_REPO;
|
||||
export const VITE_GITHUB_CLIENT_ID = import.meta.env.VITE_GITHUB_CLIENT_ID;
|
||||
export const VITE_WALLET_CONNECT_ID = import.meta.env.VITE_WALLET_CONNECT_ID;
|
||||
export const VITE_BUGSNAG_API_KEY = import.meta.env.VITE_BUGSNAG_API_KEY;
|
||||
export const VITE_LIT_RELAY_API_KEY = import.meta.env.VITE_LIT_RELAY_API_KEY;
|
||||
export const VITE_LACONICD_CHAIN_ID = import.meta.env.VITE_LACONICD_CHAIN_ID;
|
||||
|
@ -1,8 +0,0 @@
|
||||
import { WalletConnectModal } from '@walletconnect/modal';
|
||||
|
||||
import { VITE_WALLET_CONNECT_ID } from 'utils/constants';
|
||||
|
||||
export const walletConnectModal = new WalletConnectModal({
|
||||
projectId: VITE_WALLET_CONNECT_ID,
|
||||
chains: [],
|
||||
});
|
Loading…
Reference in New Issue
Block a user