Auto sign-in using laconic wallet and show link to Laconic store on low balance #52
@ -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_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_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_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_LACONICD_CHAIN_ID = 'LACONIC_HOSTED_CONFIG_laconicd_chain_id'
|
||||||
VITE_WALLET_IFRAME_URL = 'LACONIC_HOSTED_CONFIG_wallet_iframe_url'
|
VITE_WALLET_IFRAME_URL = 'LACONIC_HOSTED_CONFIG_wallet_iframe_url'
|
||||||
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
|
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;
|
export default router;
|
||||||
|
@ -133,7 +133,6 @@ record:
|
|||||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
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_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_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_laconicd_chain_id: laconic-testnet-2
|
||||||
LACONIC_HOSTED_CONFIG_wallet_iframe_url: https://wallet.laconic.com
|
LACONIC_HOSTED_CONFIG_wallet_iframe_url: https://wallet.laconic.com
|
||||||
meta:
|
meta:
|
||||||
|
@ -127,7 +127,6 @@ record:
|
|||||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
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_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_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_laconicd_chain_id: laconic-testnet-2
|
||||||
meta:
|
meta:
|
||||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
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_IMAGE_UPLOAD_PWA_TEMPLATE_REPO="snowball-tools/image-upload-pwa-example"
|
||||||
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO="snowball-tools/starter.nextjs-react-tailwind"
|
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO="snowball-tools/starter.nextjs-react-tailwind"
|
||||||
|
|
||||||
VITE_WALLET_CONNECT_ID=
|
|
||||||
|
|
||||||
VITE_LIT_RELAY_API_KEY=
|
VITE_LIT_RELAY_API_KEY=
|
||||||
|
|
||||||
VITE_BUGSNAG_API_KEY=
|
VITE_BUGSNAG_API_KEY=
|
||||||
|
@ -41,13 +41,12 @@
|
|||||||
"@turnkey/http": "^2.10.0",
|
"@turnkey/http": "^2.10.0",
|
||||||
"@turnkey/sdk-react": "^0.1.0",
|
"@turnkey/sdk-react": "^0.1.0",
|
||||||
"@turnkey/webauthn-stamper": "^0.5.0",
|
"@turnkey/webauthn-stamper": "^0.5.0",
|
||||||
"@walletconnect/ethereum-provider": "^2.16.1",
|
|
||||||
"@web3modal/siwe": "4.0.5",
|
"@web3modal/siwe": "4.0.5",
|
||||||
"@web3modal/wagmi": "4.0.5",
|
|
||||||
"assert": "^2.1.0",
|
"assert": "^2.1.0",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
|
"ethers": "^5.6.2",
|
||||||
"downshift": "^8.3.2",
|
"downshift": "^8.3.2",
|
||||||
"framer-motion": "^11.0.8",
|
"framer-motion": "^11.0.8",
|
||||||
"gql-client": "^1.0.0",
|
"gql-client": "^1.0.0",
|
||||||
@ -69,7 +68,6 @@
|
|||||||
"usehooks-ts": "^2.15.1",
|
"usehooks-ts": "^2.15.1",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"viem": "^2.7.11",
|
"viem": "^2.7.11",
|
||||||
"wagmi": "2.5.7",
|
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -11,8 +11,8 @@ import ProjectSearchLayout from './layouts/ProjectSearch';
|
|||||||
import Index from './pages';
|
import Index from './pages';
|
||||||
import AuthPage from './pages/AuthPage';
|
import AuthPage from './pages/AuthPage';
|
||||||
import { DashboardLayout } from './pages/org-slug/layout';
|
import { DashboardLayout } from './pages/org-slug/layout';
|
||||||
import Web3Provider from 'context/Web3Provider';
|
|
||||||
import { BASE_URL } from 'utils/constants';
|
import { BASE_URL } from 'utils/constants';
|
||||||
|
import BuyPrepaidService from './pages/BuyPrepaidService';
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -50,6 +50,10 @@ const router = createBrowserRouter([
|
|||||||
path: '/login',
|
path: '/login',
|
||||||
element: <AuthPage />,
|
element: <AuthPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/buy-prepaid-service',
|
||||||
|
element: <BuyPrepaidService />,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@ -75,9 +79,7 @@ function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Web3Provider>
|
<RouterProvider router={router} />
|
||||||
<RouterProvider router={router} />
|
|
||||||
</Web3Provider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
VITE_WALLET_IFRAME_URL,
|
||||||
} from 'utils/constants';
|
} from 'utils/constants';
|
||||||
|
|
||||||
const IFrameModal = ({
|
const ApproveTransactionModal = ({
|
||||||
setAccounts,
|
setAccount,
|
||||||
setIsDataReceived,
|
setIsDataReceived,
|
||||||
isVisible,
|
isVisible,
|
||||||
}: {
|
}: {
|
||||||
setAccounts: (accounts: string[]) => void;
|
setAccount: (account: string) => void;
|
||||||
setIsDataReceived: (isReceived: boolean) => void;
|
setIsDataReceived: (isReceived: boolean) => void;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
@ -20,10 +20,18 @@ const IFrameModal = ({
|
|||||||
const handleMessage = (event: MessageEvent) => {
|
const handleMessage = (event: MessageEvent) => {
|
||||||
if (event.origin !== VITE_WALLET_IFRAME_URL) return;
|
if (event.origin !== VITE_WALLET_IFRAME_URL) return;
|
||||||
|
|
||||||
setIsDataReceived(true);
|
|
||||||
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
|
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
|
||||||
setAccounts(event.data.data);
|
setIsDataReceived(true);
|
||||||
} else if (event.data.type === 'ERROR') {
|
|
||||||
|
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);
|
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 { useForm, Controller } from 'react-hook-form';
|
||||||
import { FormProvider, FieldValues } from 'react-hook-form';
|
import { FormProvider, FieldValues } from 'react-hook-form';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
@ -8,6 +8,7 @@ import {
|
|||||||
AuctionParams,
|
AuctionParams,
|
||||||
Deployer,
|
Deployer,
|
||||||
} from 'gql-client';
|
} from 'gql-client';
|
||||||
|
import { BigNumber } from 'ethers';
|
||||||
|
|
||||||
import { Select, MenuItem, FormControl, FormHelperText } from '@mui/material';
|
import { Select, MenuItem, FormControl, FormHelperText } from '@mui/material';
|
||||||
|
|
||||||
@ -20,14 +21,14 @@ import { Button } from '../../shared/Button';
|
|||||||
import { Input } from 'components/shared/Input';
|
import { Input } from 'components/shared/Input';
|
||||||
import { useToast } from 'components/shared/Toast';
|
import { useToast } from 'components/shared/Toast';
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||||
import IFrameModal from './IFrameModal';
|
import ApproveTransactionModal from './ApproveTransactionModal';
|
||||||
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
|
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
|
||||||
import { EnvironmentVariablesFormValues } from 'types/types';
|
import { EnvironmentVariablesFormValues } from 'types/types';
|
||||||
import {
|
import {
|
||||||
VITE_LACONICD_CHAIN_ID,
|
VITE_LACONICD_CHAIN_ID,
|
||||||
VITE_WALLET_IFRAME_URL,
|
VITE_WALLET_IFRAME_URL,
|
||||||
} from 'utils/constants';
|
} from 'utils/constants';
|
||||||
import AccountsDropdown from './AccountsDropdown';
|
import CheckBalanceIframe from './CheckBalanceIframe';
|
||||||
|
|
||||||
type ConfigureDeploymentFormValues = {
|
type ConfigureDeploymentFormValues = {
|
||||||
option: string;
|
option: string;
|
||||||
@ -46,12 +47,13 @@ const Configure = () => {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [deployers, setDeployers] = useState<Deployer[]>([]);
|
const [deployers, setDeployers] = useState<Deployer[]>([]);
|
||||||
const [selectedAccount, setSelectedAccount] = useState<string>();
|
const [selectedAccount, setSelectedAccount] = useState<string>();
|
||||||
const [accounts, setAccounts] = useState<string[]>([]);
|
|
||||||
const [selectedDeployer, setSelectedDeployer] = useState<Deployer>();
|
const [selectedDeployer, setSelectedDeployer] = useState<Deployer>();
|
||||||
const [isPaymentLoading, setIsPaymentLoading] = useState(false);
|
const [isPaymentLoading, setIsPaymentLoading] = useState(false);
|
||||||
const [isPaymentDone, setIsPaymentDone] = useState(false);
|
const [isPaymentDone, setIsPaymentDone] = useState(false);
|
||||||
const [isFrameVisible, setIsFrameVisible] = useState(false);
|
const [isFrameVisible, setIsFrameVisible] = useState(false);
|
||||||
const [isAccountsDataReceived, setIsAccountsDataReceived] = useState(false);
|
const [isAccountsDataReceived, setIsAccountsDataReceived] = useState(false);
|
||||||
|
const [balanceMessage, setBalanceMessage] = useState<string>();
|
||||||
|
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>();
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const templateId = searchParams.get('templateId');
|
const templateId = searchParams.get('templateId');
|
||||||
@ -81,10 +83,33 @@ const Configure = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const selectedOption = methods.watch('option');
|
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 isTabletView = useMediaQuery('(min-width: 720px)'); // md:
|
||||||
const buttonSize = isTabletView ? { size: 'lg' as const } : {};
|
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 (
|
const createProject = async (
|
||||||
data: FieldValues,
|
data: FieldValues,
|
||||||
envVariables: AddEnvironmentVariableInput[],
|
envVariables: AddEnvironmentVariableInput[],
|
||||||
@ -186,7 +211,6 @@ const Configure = () => {
|
|||||||
(deployer) => deployer.deployerLrn === deployerLrn,
|
(deployer) => deployer.deployerLrn === deployerLrn,
|
||||||
);
|
);
|
||||||
|
|
||||||
let amount: string;
|
|
||||||
let senderAddress: string;
|
let senderAddress: string;
|
||||||
let txHash: string | null = null;
|
let txHash: string | null = null;
|
||||||
if (createFormData.option === 'LRN' && !deployer?.minimumPayment) {
|
if (createFormData.option === 'LRN' && !deployer?.minimumPayment) {
|
||||||
@ -204,16 +228,6 @@ const Configure = () => {
|
|||||||
|
|
||||||
senderAddress = selectedAccount;
|
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);
|
txHash = await cosmosSendTokensHandler(senderAddress, amountToBePaid);
|
||||||
|
|
||||||
if (!txHash) {
|
if (!txHash) {
|
||||||
@ -303,7 +317,7 @@ const Configure = () => {
|
|||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[client, createProject, dismiss, toast],
|
[client, createProject, dismiss, toast, amountToBePaid],
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchDeployers = useCallback(async () => {
|
const fetchDeployers = useCallback(async () => {
|
||||||
@ -311,10 +325,6 @@ const Configure = () => {
|
|||||||
setDeployers(res.deployers);
|
setDeployers(res.deployers);
|
||||||
}, [client]);
|
}, [client]);
|
||||||
|
|
||||||
const onAccountChange = useCallback((account: string) => {
|
|
||||||
setSelectedAccount(account);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onDeployerChange = useCallback(
|
const onDeployerChange = useCallback(
|
||||||
(selectedLrn: string) => {
|
(selectedLrn: string) => {
|
||||||
const deployer = deployers.find((d) => d.deployerLrn === selectedLrn);
|
const deployer = deployers.find((d) => d.deployerLrn === selectedLrn);
|
||||||
@ -340,12 +350,20 @@ const Configure = () => {
|
|||||||
await requestTx(senderAddress, snowballAddress, amount);
|
await requestTx(senderAddress, snowballAddress, amount);
|
||||||
|
|
||||||
const txHash = await new Promise<string | null>((resolve, reject) => {
|
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) => {
|
const handleTxStatus = async (event: MessageEvent) => {
|
||||||
if (event.origin !== VITE_WALLET_IFRAME_URL) return;
|
if (event.origin !== VITE_WALLET_IFRAME_URL) return;
|
||||||
|
|
||||||
if (event.data.type === 'TRANSACTION_RESPONSE') {
|
if (event.data.type === 'TRANSACTION_RESPONSE') {
|
||||||
const txResponse = event.data.data;
|
const txResponse = event.data.data;
|
||||||
resolve(txResponse);
|
resolve(txResponse);
|
||||||
|
|
||||||
|
cleanup();
|
||||||
} else if (event.data.type === 'ERROR') {
|
} else if (event.data.type === 'ERROR') {
|
||||||
console.error('Error from wallet:', event.data.message);
|
console.error('Error from wallet:', event.data.message);
|
||||||
reject(new Error('Transaction failed'));
|
reject(new Error('Transaction failed'));
|
||||||
@ -355,10 +373,9 @@ const Configure = () => {
|
|||||||
variant: 'error',
|
variant: 'error',
|
||||||
onDismiss: dismiss,
|
onDismiss: dismiss,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
setIsFrameVisible(false);
|
|
||||||
|
|
||||||
window.removeEventListener('message', handleTxStatus);
|
cleanup();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('message', handleTxStatus);
|
window.addEventListener('message', handleTxStatus);
|
||||||
@ -418,6 +435,12 @@ const Configure = () => {
|
|||||||
fetchDeployers();
|
fetchDeployers();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isBalanceSufficient) {
|
||||||
|
setBalanceMessage(undefined);
|
||||||
|
}
|
||||||
|
}, [isBalanceSufficient]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-7 px-4 py-6">
|
<div className="space-y-7 px-4 py-6">
|
||||||
<div className="flex justify-between mb-6">
|
<div className="flex justify-between mb-6">
|
||||||
@ -579,49 +602,81 @@ const Configure = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="flex gap-4">
|
||||||
<AccountsDropdown
|
<Button
|
||||||
accounts={accounts}
|
{...buttonSize}
|
||||||
onAccountChange={onAccountChange}
|
type="submit"
|
||||||
isDataReceived={isAccountsDataReceived}
|
shape="default"
|
||||||
/>
|
disabled={
|
||||||
{accounts.length > 0 && (
|
isLoading ||
|
||||||
<div>
|
isPaymentLoading ||
|
||||||
<Button
|
!selectedAccount ||
|
||||||
{...buttonSize}
|
!isBalanceSufficient
|
||||||
type="submit"
|
}
|
||||||
shape="default"
|
rightIcon={
|
||||||
disabled={
|
isLoading || isPaymentLoading ? (
|
||||||
isLoading || isPaymentLoading || !selectedAccount
|
<LoadingIcon className="animate-spin" />
|
||||||
}
|
) : (
|
||||||
rightIcon={
|
<ArrowRightCircleFilledIcon />
|
||||||
isLoading || isPaymentLoading ? (
|
)
|
||||||
<LoadingIcon className="animate-spin" />
|
}
|
||||||
|
>
|
||||||
|
{!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 />
|
'Insufficient funds.'
|
||||||
)
|
)}
|
||||||
}
|
</p>
|
||||||
>
|
</div>
|
||||||
{!isPaymentDone
|
) : null
|
||||||
? isPaymentLoading
|
) : null}
|
||||||
? 'Transaction Requested'
|
</div>
|
||||||
: 'Pay and Deploy'
|
|
||||||
: isLoading
|
|
||||||
? 'Deploying'
|
|
||||||
: 'Deploy'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|
||||||
<IFrameModal
|
<ApproveTransactionModal
|
||||||
setAccounts={setAccounts}
|
setAccount={setSelectedAccount}
|
||||||
setIsDataReceived={setIsAccountsDataReceived}
|
setIsDataReceived={setIsAccountsDataReceived}
|
||||||
isVisible={isFrameVisible}
|
isVisible={isFrameVisible}
|
||||||
/>
|
/>
|
||||||
|
<CheckBalanceIframe
|
||||||
|
onBalanceChange={setIsBalanceSufficient}
|
||||||
|
amount={amountToBePaid}
|
||||||
|
isPollingEnabled={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
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 { User } from 'gql-client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useDisconnect } from 'wagmi';
|
|
||||||
|
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
import {
|
import {
|
||||||
GlobeIcon,
|
|
||||||
LifeBuoyIcon,
|
LifeBuoyIcon,
|
||||||
LogoutIcon,
|
|
||||||
QuestionMarkRoundIcon,
|
QuestionMarkRoundIcon,
|
||||||
} from 'components/shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
import { Tabs } from 'components/shared/Tabs';
|
import { Tabs } from 'components/shared/Tabs';
|
||||||
@ -16,10 +13,9 @@ import { Logo } from 'components/Logo';
|
|||||||
import { Avatar } from 'components/shared/Avatar';
|
import { Avatar } from 'components/shared/Avatar';
|
||||||
import { formatAddress } from 'utils/format';
|
import { formatAddress } from 'utils/format';
|
||||||
import { getInitials } from 'utils/geInitials';
|
import { getInitials } from 'utils/geInitials';
|
||||||
import { Button } from 'components/shared/Button';
|
|
||||||
import { cn } from 'utils/classnames';
|
import { cn } from 'utils/classnames';
|
||||||
import { useMediaQuery } from 'usehooks-ts';
|
import { useMediaQuery } from 'usehooks-ts';
|
||||||
import { BASE_URL } from 'utils/constants';
|
import { SHOPIFY_APP_URL } from '../../../constants';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
mobileOpen?: boolean;
|
mobileOpen?: boolean;
|
||||||
@ -27,12 +23,10 @@ interface SidebarProps {
|
|||||||
|
|
||||||
export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
||||||
const { orgSlug } = useParams();
|
const { orgSlug } = useParams();
|
||||||
const navigate = useNavigate();
|
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
const isDesktop = useMediaQuery('(min-width: 960px)');
|
const isDesktop = useMediaQuery('(min-width: 960px)');
|
||||||
|
|
||||||
const [user, setUser] = useState<User>();
|
const [user, setUser] = useState<User>();
|
||||||
const { disconnect } = useDisconnect();
|
|
||||||
|
|
||||||
const fetchUser = useCallback(async () => {
|
const fetchUser = useCallback(async () => {
|
||||||
const { user } = await client.getUser();
|
const { user } = await client.getUser();
|
||||||
@ -43,16 +37,6 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
|||||||
fetchUser();
|
fetchUser();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleLogOut = useCallback(async () => {
|
|
||||||
await fetch(`${BASE_URL}/auth/logout`, {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
localStorage.clear();
|
|
||||||
disconnect();
|
|
||||||
navigate('/login');
|
|
||||||
}, [disconnect, navigate]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.nav
|
<motion.nav
|
||||||
initial={{ x: -320 }}
|
initial={{ x: -320 }}
|
||||||
@ -82,19 +66,10 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
|||||||
<Tabs defaultValue="Projects" orientation="vertical">
|
<Tabs defaultValue="Projects" orientation="vertical">
|
||||||
{/* // TODO: use proper link buttons */}
|
{/* // TODO: use proper link buttons */}
|
||||||
<Tabs.List>
|
<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="">
|
<Tabs.Trigger icon={<QuestionMarkRoundIcon />} value="">
|
||||||
<a
|
<a
|
||||||
className="cursor-pointer font-mono"
|
className="cursor-pointer font-mono"
|
||||||
href="https://store.laconic.com/pages/instruction-faq"
|
href={`${SHOPIFY_APP_URL}/pages/instruction-faq`}
|
||||||
>
|
>
|
||||||
DOCUMENTATION
|
DOCUMENTATION
|
||||||
</a>
|
</a>
|
||||||
@ -125,14 +100,6 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button
|
|
||||||
iconOnly
|
|
||||||
variant="ghost"
|
|
||||||
className="text-elements-low-em"
|
|
||||||
onClick={handleLogOut}
|
|
||||||
>
|
|
||||||
<LogoutIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.nav>
|
</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 SHORT_COMMIT_HASH_LENGTH = 8;
|
||||||
|
|
||||||
export const SERVER_GQL_PATH = 'graphql';
|
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 { Toaster } from 'components/shared/Toast';
|
||||||
import { LogErrorBoundary } from 'utils/log-error';
|
import { LogErrorBoundary } from 'utils/log-error';
|
||||||
import { BASE_URL } from 'utils/constants';
|
import { BASE_URL } from 'utils/constants';
|
||||||
import Web3ModalProvider from './context/Web3Provider';
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
console.log(`v-0.0.9`);
|
console.log(`v-0.0.9`);
|
||||||
@ -32,12 +31,10 @@ root.render(
|
|||||||
<LogErrorBoundary>
|
<LogErrorBoundary>
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Web3ModalProvider>
|
<GQLClientProvider client={gqlClient}>
|
||||||
<GQLClientProvider client={gqlClient}>
|
<App />
|
||||||
<App />
|
<Toaster />
|
||||||
<Toaster />
|
</GQLClientProvider>
|
||||||
</GQLClientProvider>
|
|
||||||
</Web3ModalProvider>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
</LogErrorBoundary>,
|
</LogErrorBoundary>,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Login } from './auth/Login';
|
import AutoSignInIFrameModal from 'components/shared/auth/AutoSignInIFrameModal';
|
||||||
|
|
||||||
const AuthPage = () => {
|
const AuthPage = () => {
|
||||||
return (
|
return (
|
||||||
@ -13,9 +13,7 @@ const AuthPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pb-12 relative z-10 flex-1 flex-center">
|
<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">
|
<AutoSignInIFrameModal />
|
||||||
<Login />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
<WavyBorder className="self-stretch" variant="stroke" />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
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 { ProjectCard } from 'components/projects/ProjectCard';
|
||||||
import { Heading, Badge, Button } from 'components/shared';
|
import { Heading, Badge, Button } from 'components/shared';
|
||||||
import { PlusIcon } from 'components/shared/CustomIcon';
|
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
import { Project } from 'gql-client';
|
import { Project } from 'gql-client';
|
||||||
|
import CheckBalanceIframe from 'components/projects/create/CheckBalanceIframe';
|
||||||
|
|
||||||
const Projects = () => {
|
const Projects = () => {
|
||||||
|
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>();
|
||||||
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
const { orgSlug } = useParams();
|
const { orgSlug } = useParams();
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
|
||||||
|
|
||||||
const fetchProjects = useCallback(async () => {
|
const fetchProjects = useCallback(async () => {
|
||||||
const { projectsInOrganization } = await client.getProjectsInOrganization(
|
const { projectsInOrganization } = await client.getProjectsInOrganization(
|
||||||
@ -23,6 +27,12 @@ const Projects = () => {
|
|||||||
fetchProjects();
|
fetchProjects();
|
||||||
}, [orgSlug]);
|
}, [orgSlug]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isBalanceSufficient === false) {
|
||||||
|
navigate('/buy-prepaid-service');
|
||||||
|
}
|
||||||
|
}, [isBalanceSufficient]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
|
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@ -49,6 +59,8 @@ const Projects = () => {
|
|||||||
return <ProjectCard project={project} key={key} />;
|
return <ProjectCard project={project} key={key} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CheckBalanceIframe onBalanceChange={setIsBalanceSufficient} isPollingEnabled={false} amount='1' />
|
||||||
</section>
|
</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
|
export const VITE_GITHUB_NEXT_APP_TEMPLATE_REPO = import.meta.env
|
||||||
.VITE_GITHUB_NEXT_APP_TEMPLATE_REPO;
|
.VITE_GITHUB_NEXT_APP_TEMPLATE_REPO;
|
||||||
export const VITE_GITHUB_CLIENT_ID = import.meta.env.VITE_GITHUB_CLIENT_ID;
|
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_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_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;
|
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