prettier config, dwnld button, dwnld content, chain login > purchase, selectWallet post in useAuth

This commit is contained in:
Verticool 2025-03-16 20:38:43 +06:00
parent 5f4b13d7ba
commit 2351ee51eb
9 changed files with 409 additions and 314 deletions

7
.prettierrc.json Normal file
View File

@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 4,
"printWidth": 100,
"trailingComma": "es5"
}

View File

@ -3,18 +3,18 @@ import { Outlet } from 'react-router-dom';
import { useAuthTwa } from '~/shared/services/authTwa'; import { useAuthTwa } from '~/shared/services/authTwa';
export const ProtectedLayout = () => { export const ProtectedLayout = () => {
const auth = useAuthTwa(); const auth = useAuthTwa();
const [isInitialLoad, setIsInitialLoad] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true);
useEffect(() => { useEffect(() => {
void auth.mutateAsync().finally(() => { void auth.mutateAsync().finally(() => {
setIsInitialLoad(false); setIsInitialLoad(false);
}); });
}, []); }, []);
if (isInitialLoad || auth.isLoading) { if (isInitialLoad || auth.isLoading) {
return null; return null;
} }
return <Outlet />; return <Outlet />;
}; };

View File

@ -165,6 +165,16 @@ export const DataStep = ({ nextStep }: DataStepProps) => {
</FormLabel> </FormLabel>
<div className={"flex flex-col gap-2"}> <div className={"flex flex-col gap-2"}>
<FormLabel
label={"Разрешить скачивание"}
labelClassName={"flex"}
formLabelAddon={
<Checkbox
onClick={() => rootStore.setAllowDwnld(!rootStore.allowDwnld)}
checked={rootStore.allowDwnld}
/>
}
/>
<FormLabel <FormLabel
label={"Разрешить обложку"} label={"Разрешить обложку"}
labelClassName={"flex"} labelClassName={"flex"}

View File

@ -75,7 +75,7 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
// Откомментировать при условии того что вы принимаете много авторов // Откомментировать при условии того что вы принимаете много авторов
// следует отметить что вы должны еще откомментровать AuthorsStep в RootPage // следует отметить что вы должны еще откомментровать AuthorsStep в RootPage
// authors: rootStore.authors, // authors: rootStore.authors,
downloadable: rootStore.allowDwnld,
content: fileUploadResult.content_id_v1, content: fileUploadResult.content_id_v1,
image: coverUploadResult.content_id_v1, image: coverUploadResult.content_id_v1,
price: String(rootStore.price * 10 ** 9), price: String(rootStore.price * 10 ** 9),

View File

@ -1,217 +1,275 @@
import ReactPlayer from "react-player/lazy"; import ReactPlayer from 'react-player/lazy';
import { useTonConnectUI } from "@tonconnect/ui-react"; import { useTonConnectUI } from '@tonconnect/ui-react';
import { useWebApp } from "@vkruglikov/react-telegram-web-app"; import { useWebApp } from '@vkruglikov/react-telegram-web-app';
import { Button } from "~/shared/ui/button"; import { Button } from '~/shared/ui/button';
import { usePurchaseContent, useViewContent } from "~/shared/services/content"; import { usePurchaseContent, useViewContent } from '~/shared/services/content';
import { fromNanoTON } from "~/shared/utils"; import { fromNanoTON } from '~/shared/utils';
import {useCallback, useEffect, useMemo, useState} from "react"; import { useCallback, useEffect, useMemo, useState } from 'react';
import { AudioPlayer } from "~/shared/ui/audio-player"; import { AudioPlayer } from '~/shared/ui/audio-player';
import {useAuth} from "~/shared/services/auth"; import { useAuth } from '~/shared/services/auth';
import { CongratsModal } from "./components/congrats-modal"; import { CongratsModal } from './components/congrats-modal';
import { ErrorModal } from "./components/error-modal"; import { ErrorModal } from './components/error-modal';
type InvoiceStatus = 'paid' | 'failed' | 'cancelled' | 'pending'; type InvoiceStatus = 'paid' | 'failed' | 'cancelled' | 'pending';
// Add type for invoice event // Add type for invoice event
interface InvoiceEvent { interface InvoiceEvent {
url: string; url: string;
status: InvoiceStatus; status: InvoiceStatus;
} }
export const ViewContentPage = () => { export const ViewContentPage = () => {
const WebApp = useWebApp(); const WebApp = useWebApp();
const { data: content, refetch: refetchContent } = useViewContent(WebApp.initDataUnsafe?.start_param); const { data: content, refetch: refetchContent } = useViewContent(
WebApp.initDataUnsafe?.start_param
);
const { mutateAsync: purchaseContent } = usePurchaseContent(); const { mutateAsync: purchaseContent } = usePurchaseContent();
const [tonConnectUI] = useTonConnectUI(); const [tonConnectUI] = useTonConnectUI();
const auth = useAuth(); const auth = useAuth();
const [isCongratsModal, setIsCongratsModal] = useState(false);
const [isErrorModal, setIsErrorModal] = useState(false);
const [isCongratsModal, setIsCongratsModal] = useState(false); const handleBuyContentTON = useCallback(async () => {
const [isErrorModal, setIsErrorModal] = useState(false); try {
const handleBuyContentTON = useCallback(async () => { // Helper function to wait for wallet connection
try { const waitForConnection = async (timeoutMs = 30000, intervalMs = 500) => {
if (!tonConnectUI.connected) { const startTime = Date.now();
await tonConnectUI.openModal();
await auth.mutateAsync();
return
} else {
await auth.mutateAsync()
}
const contentResponse = await purchaseContent({ while (Date.now() - startTime < timeoutMs) {
content_address: WebApp.initDataUnsafe?.start_param, if (tonConnectUI.connected) {
license_type: "resale", return true;
}); }
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
const transactionResponse = await tonConnectUI.sendTransaction({ return false; // Timed out
validUntil: Math.floor(Date.now() / 1000) + 120, };
messages: [
{
amount: contentResponse.data.amount,
address: contentResponse.data.address,
payload: contentResponse.data.payload,
},
],
});
if (transactionResponse.boc) { // If not connected, start connection process
void refetchContent() if (!tonConnectUI.connected) {
setIsCongratsModal(true); console.log('DEBUG: Wallet not connected, opening modal');
console.log(transactionResponse.boc, "PURCHASED")
} else {
setIsErrorModal(true);
console.error("Transaction failed:", transactionResponse);
}
} catch (error) {
setIsErrorModal(true);
console.error("Error handling Ton Connect subscription:", error);
}
}, [content, tonConnectUI.connected]);
const handleBuyContentStars = useCallback(async () => { // Open connection modal
try { await tonConnectUI.openModal();
if (!content?.data?.invoice.url) {
console.error('No invoice URL available'); // Wait for connection
return; const connected = await waitForConnection();
} if (!connected) {
console.log('DEBUG: Connection timed out or was cancelled');
// Add event listener for invoice closing with typed event return;
const handleInvoiceClosed = (event: InvoiceEvent) => { }
if (event.url === content.data.invoice.url) {
if (event.status === 'paid') { console.log('DEBUG: Connection successful, authenticating');
void refetchContent(); await auth.mutateAsync();
setIsCongratsModal(true); } else {
} else if (event.status === 'failed' || event.status === 'cancelled') { // Already connected, just authenticate
// setIsErrorModal(true); // Turn on if need in error modal. Update text in it to match both way of payment errors await auth.mutateAsync();
} }
// Proceed with purchase
console.log('DEBUG: Proceeding with purchase');
const contentResponse = await purchaseContent({
content_address: WebApp.initDataUnsafe?.start_param,
license_type: 'resale',
});
const transactionResponse = await tonConnectUI.sendTransaction({
validUntil: Math.floor(Date.now() / 1000) + 86400, // 24 hours
messages: [
{
amount: contentResponse.data.amount,
address: contentResponse.data.address,
payload: contentResponse.data.payload,
},
],
});
if (transactionResponse.boc) {
void refetchContent();
setIsCongratsModal(true);
console.log(transactionResponse.boc, 'PURCHASED');
} else {
setIsErrorModal(true);
console.error('Transaction failed:', transactionResponse);
}
} catch (error) {
setIsErrorModal(true);
console.error('Error handling Ton Connect subscription:', error);
} }
}; }, [auth, purchaseContent, refetchContent, tonConnectUI, WebApp.initDataUnsafe?.start_param]);
WebApp.onEvent('invoiceClosed', handleInvoiceClosed); const handleBuyContentStars = useCallback(async () => {
try {
await WebApp.openInvoice( if (!content?.data?.invoice.url) {
content.data.invoice.url, console.error('No invoice URL available');
(status: InvoiceStatus) => { return;
console.log('Invoice status:', status); }
if (status === 'paid') {
// Add event listener for invoice closing with typed event
const handleInvoiceClosed = (event: InvoiceEvent) => {
if (event.url === content.data.invoice.url) {
if (event.status === 'paid') {
void refetchContent();
setIsCongratsModal(true);
} else if (event.status === 'failed' || event.status === 'cancelled') {
// setIsErrorModal(true); // Turn on if need in error modal. Update text in it to match both way of payment errors
}
}
};
WebApp.onEvent('invoiceClosed', handleInvoiceClosed);
await WebApp.openInvoice(content.data.invoice.url, (status: InvoiceStatus) => {
console.log('Invoice status:', status);
if (status === 'paid') {
void refetchContent();
setIsCongratsModal(true);
} else if (status === 'failed' || status === 'cancelled') {
// setIsErrorModal(true); // Turn on if need in error modal. Update text in it to match both way of payment errors
}
});
return () => {
WebApp.offEvent('invoiceClosed', handleInvoiceClosed);
};
} catch (error) {
console.error('Payment failed:', error);
// setIsErrorModal(true); // Turn on if need in error modal. Update text in it to match both way of payment errors
}
}, [content, refetchContent]);
const haveLicense = useMemo(() => {
document.title = content?.data?.display_options?.metadata?.name;
return (
content?.data?.have_licenses?.includes('listen') ||
content?.data?.have_licenses?.includes('resale')
);
}, [content]);
useEffect(() => {
const interval = setInterval(() => {
void refetchContent(); void refetchContent();
setIsCongratsModal(true); }, 5000);
} else if (status === 'failed' || status === 'cancelled') {
// setIsErrorModal(true); // Turn on if need in error modal. Update text in it to match both way of payment errors return () => clearInterval(interval);
} }, []);
const handleConfirmCongrats = () => {
setIsCongratsModal(!isCongratsModal);
};
const handleErrorModal = () => {
setIsErrorModal(!isErrorModal);
};
const handleDwnldContent = async () => {
try {
const fileUrl = content?.data?.display_options?.content_url;
const fileName = content?.data?.display_options?.metadata?.name || 'content';
const fileFormat = content?.data?.content_type?.contentFormat || '';
await WebApp.downloadFile({
url: fileUrl,
file_name: fileName + fileFormat,
});
} catch (error) {
console.error('Error downloading content:', error);
} }
); };
return () => {
WebApp.offEvent('invoiceClosed', handleInvoiceClosed);
};
} catch (error) {
console.error('Payment failed:', error);
// setIsErrorModal(true); // Turn on if need in error modal. Update text in it to match both way of payment errors
}
}, [content, refetchContent]);
const haveLicense = useMemo(() => { return (
document.title = content?.data?.display_options?.metadata?.name; <main className={'min-h-screen flex w-full flex-col gap-[50px] px-4 '}>
return content?.data?.have_licenses?.includes("listen") || content?.data?.have_licenses?.includes("resale"); {isCongratsModal && <CongratsModal onConfirm={handleConfirmCongrats} />}
}, [content]) {isErrorModal && <ErrorModal onConfirm={handleErrorModal} />}
{content?.data?.content_type.startsWith('audio') &&
content?.data?.display_options?.metadata?.image && (
<div className={'mt-[30px] h-[314px] w-full'}>
<img
alt={'content_image'}
className={'h-full w-full object-cover object-center'}
src={content?.data?.display_options?.metadata?.image}
/>
</div>
)}
useEffect(() => { {content?.data?.content_type.startsWith('audio') ? (
const interval = setInterval(() => { <AudioPlayer src={content?.data?.display_options?.content_url} />
void refetchContent() ) : (
}, 5000) <ReactPlayer
playsinline={true}
controls={true}
width="100%"
config={{
file: {
attributes: {
playsInline: true,
autoPlay: true,
poster:
content?.data?.display_options?.metadata?.image || undefined,
},
},
}}
url={content?.data?.display_options?.content_url}
/>
)}
return () => clearInterval(interval) <section className={'flex flex-col'}>
}, []); <h1 className={'text-[20px] font-bold'}>
{content?.data?.display_options?.metadata?.name}
</h1>
{/*<h2>Russian</h2>*/}
{/*<h2>2022</h2>*/}
<p className={'mt-2 text-[12px]'}>
{content?.data?.display_options?.metadata?.description}
</p>
</section>
const handleConfirmCongrats = () => { <div className="mt-auto pb-2">
setIsCongratsModal(!isCongratsModal); {content?.data?.downloadable && (
}; <Button
onClick={() => handleDwnldContent()}
const handleErrorModal = () => { className={'h-[48px] bg-darkred mb-4'}
setIsErrorModal(!isErrorModal); label={`Скачать контент`}
} />
return ( )}
<main className={"min-h-screen flex w-full flex-col gap-[50px] px-4 "}> {!haveLicense && (
{isCongratsModal && <CongratsModal <div className="flex flex-row gap-4">
onConfirm={handleConfirmCongrats}/>} <Button
{isErrorModal && <ErrorModal onClick={handleBuyContentTON}
onConfirm={handleErrorModal}/>} className={'mb-4 h-[48px] px-2'}
{content?.data?.content_type.startsWith("audio") && content?.data?.display_options?.metadata?.image && ( label={`Купить за ${fromNanoTON(content?.data?.encrypted?.license?.resale?.price)} ТОН`}
<div className={"mt-[30px] h-[314px] w-full"}> includeArrows={content?.data?.invoice ? false : true}
<img />
alt={"content_image"} {content?.data?.invoice && (
className={"h-full w-full object-cover object-center"} <Button
src={content?.data?.display_options?.metadata?.image} onClick={handleBuyContentStars}
/> className={'mb-4 h-[48px] px-2'}
</div> label={`Купить за ${content?.data?.invoice?.amount} ⭐️`}
)} />
)}
{content?.data?.content_type.startsWith("audio") ? ( </div>
<AudioPlayer src={content?.data?.display_options?.content_url} /> )}
) : ( <Button
<ReactPlayer onClick={() => {
playsinline={true} WebApp.openTelegramLink(`https://t.me/MY_UploaderRobot`);
controls={true} }}
width="100%" className={'h-[48px] bg-darkred'}
config={{ file: { attributes: { label={`Загрузить свой контент`}
playsInline: true, autoplay: true, />
poster: content?.data?.display_options?.metadata?.image || undefined, {tonConnectUI.connected && (
} }, }} <Button
url={content?.data?.display_options?.content_url} onClick={() => {
/> tonConnectUI.disconnect();
)} }}
className={'h-[48px] bg-darkred mt-4'}
<section className={"flex flex-col"}> label={`Отключить кошелек`}
<h1 className={"text-[20px] font-bold"}> />
{content?.data?.display_options?.metadata?.name} )}
</h1> </div>
{/*<h2>Russian</h2>*/} </main>
{/*<h2>2022</h2>*/} );
<p className={"mt-2 text-[12px]"}>
{content?.data?.display_options?.metadata?.description}
</p>
</section>
<div className="mt-auto pb-2">
{!haveLicense && <div className="flex flex-row gap-4">
<Button
onClick={handleBuyContentTON}
className={"mb-4 h-[48px] px-2"}
label={`Купить за ${fromNanoTON(content?.data?.encrypted?.license?.resale?.price)} ТОН`}
includeArrows={content?.data?.invoice ? false : true}
/>
{content?.data?.invoice && (
<Button
onClick={handleBuyContentStars}
className={"mb-4 h-[48px] px-2"}
label={`Купить за ${content?.data?.invoice?.amount} ⭐️`}
/>
)}
</div>
}
<Button
onClick={() => {
WebApp.openTelegramLink(`https://t.me/MY_UploaderRobot`);
}}
className={"h-[48px] bg-darkred"}
label={`Загрузить свой контент`}
/>
{tonConnectUI.connected && (
<Button
onClick={() => {
tonConnectUI.disconnect();
}}
className={"h-[48px] bg-darkred mt-4"}
label={`Отключить кошелек`}
/>
)}
</div>
</main>
);
}; };

View File

@ -1,17 +1,17 @@
import axios from "axios"; import axios from 'axios';
export const APP_API_BASE_URL = import.meta.env.VITE_API_BASE_URL; export const APP_API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
export const request = axios.create({ export const request = axios.create({
baseURL: APP_API_BASE_URL, baseURL: APP_API_BASE_URL,
}); });
request.interceptors.request.use((config) => { request.interceptors.request.use((config) => {
const auth_v1_token = sessionStorage.getItem("auth_v1_token"); const auth_v1_token = localStorage.getItem('auth_v1_token');
if (auth_v1_token) { if (auth_v1_token) {
config.headers.Authorization = auth_v1_token; config.headers.Authorization = auth_v1_token;
} }
return config; return config;
}); });

View File

@ -1,118 +1,131 @@
import { useRef } from "react"; import { useRef } from 'react';
import { useTonConnectUI } from "@tonconnect/ui-react"; import { useTonConnectUI } from '@tonconnect/ui-react';
import { useMutation } from "react-query"; import { useMutation } from 'react-query';
import { request } from "~/shared/libs"; import { request } from '~/shared/libs';
import { useWebApp } from "@vkruglikov/react-telegram-web-app"; import { useWebApp } from '@vkruglikov/react-telegram-web-app';
const sessionStorageKey = "auth_v1_token"; const sessionStorageKey = 'auth_v1_token';
const payloadTTLMS = 1000 * 60 * 20; const payloadTTLMS = 1000 * 60 * 20;
export const useAuth = () => { export const useAuth = () => {
const WebApp = useWebApp(); const WebApp = useWebApp();
// const wallet = useTonWallet(); // const wallet = useTonWallet();
const [tonConnectUI] = useTonConnectUI(); const [tonConnectUI] = useTonConnectUI();
const interval = useRef<ReturnType<typeof setInterval> | undefined>(); const interval = useRef<ReturnType<typeof setInterval> | undefined>();
const waitForWalletProof = async () => { const waitForWalletProof = async () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error("Timeout waiting for proof")), 30000); const timeout = setTimeout(() => reject(new Error('Timeout waiting for proof')), 30000);
const checkProof = setInterval(() => { const checkProof = setInterval(() => {
const currentWallet = tonConnectUI.wallet; const currentWallet = tonConnectUI.wallet;
if ( if (
currentWallet?.connectItems?.tonProof && currentWallet?.connectItems?.tonProof &&
!("error" in currentWallet.connectItems.tonProof) !('error' in currentWallet.connectItems.tonProof)
) { ) {
clearInterval(checkProof); clearInterval(checkProof);
clearTimeout(timeout); clearTimeout(timeout);
resolve(currentWallet.connectItems.tonProof.proof); resolve(currentWallet.connectItems.tonProof.proof);
} }
}, 500); }, 500);
});
};
const makeAuthRequest = async (params: {
twa_data: string;
ton_proof?: {
account: any;
ton_proof: any;
};
}) => {
const res = await request.post<{
connected_wallet: null | {
version: string;
address: string;
ton_balance: string;
};
auth_v1_token: string;
}>("/auth.twa", params);
if (res?.data?.auth_v1_token) {
localStorage.setItem(sessionStorageKey, res.data.auth_v1_token);
} else {
throw new Error("Failed to get auth token");
}
return res;
};
return useMutation(["auth"], async () => {
clearInterval(interval.current);
console.log("DEBUG: Starting auth flow");
// Case 1: Not connected - need to connect and get proof
if (!tonConnectUI.connected) {
console.log("DEBUG: No wallet connection, starting flow");
localStorage.removeItem(sessionStorageKey);
const refreshPayload = async () => {
tonConnectUI.setConnectRequestParameters({ state: "loading" });
const value = await request.post<{ auth_v1_token: string }>("/auth.twa", {
twa_data: WebApp.initData,
}); });
};
if (value?.data?.auth_v1_token) {
tonConnectUI.setConnectRequestParameters({ const makeAuthRequest = async (params: {
state: "ready", twa_data: string;
value: { tonProof: value.data.auth_v1_token }, ton_proof?: {
}); account: any;
ton_proof: any;
};
}) => {
const res = await request.post<{
connected_wallet: null | {
version: string;
address: string;
ton_balance: string;
};
auth_v1_token: string;
}>('/auth.twa', params);
if (res?.data?.auth_v1_token) {
localStorage.setItem(sessionStorageKey, res.data.auth_v1_token);
} else { } else {
tonConnectUI.setConnectRequestParameters(null); throw new Error('Failed to get auth token');
} }
}; return res;
};
await refreshPayload(); const makeSelectWalletRequest = async (params: { wallet_address: string }) => {
interval.current = setInterval(refreshPayload, payloadTTLMS); const res = await request.post('/auth.selectWallet', params);
return res;
};
const tonProof = await waitForWalletProof(); return useMutation(['auth'], async () => {
console.log("DEBUG: Got initial proof", tonProof); clearInterval(interval.current);
let authResult;
console.log('DEBUG: Starting auth flow');
return makeAuthRequest({ // Case 1: Not connected - need to connect and get proof
twa_data: WebApp.initData, if (!tonConnectUI.connected) {
ton_proof: { console.log('DEBUG: No wallet connection, starting flow');
account: tonConnectUI.wallet!.account, localStorage.removeItem(sessionStorageKey);
ton_proof: tonProof,
},
});
}
// Commented this part for two reasons: const refreshPayload = async () => {
// 1) When we include ton_proof from the wallet it fails the call for a reason of bad ton_proof tonConnectUI.setConnectRequestParameters({ state: 'loading' });
// 2) This call could happen only if the first case happened and it means that the ton_proof is already have been stored once before const value = await request.post<{ auth_v1_token: string }>('/auth.twa', {
// Case 2: Connected with proof - use it twa_data: WebApp.initData,
// if (wallet?.connectItems?.tonProof && !("error" in wallet.connectItems.tonProof)) { });
// console.log("DEBUG: Using existing proof", wallet.connectItems.tonProof.proof);
// return makeAuthRequest({
// twa_data: WebApp.initData,
// ton_proof: {
// account: wallet.account,
// ton_proof: wallet.connectItems.tonProof.proof,
// },
// });
// }
// Case 3: Connected without proof - already authenticated if (value?.data?.auth_v1_token) {
console.log("DEBUG: Connected without proof, proceeding without it"); tonConnectUI.setConnectRequestParameters({
return makeAuthRequest({ state: 'ready',
twa_data: WebApp.initData, value: { tonProof: value.data.auth_v1_token },
});
} else {
tonConnectUI.setConnectRequestParameters(null);
}
};
await refreshPayload();
interval.current = setInterval(refreshPayload, payloadTTLMS);
const tonProof = await waitForWalletProof();
console.log('DEBUG: Got initial proof', tonProof);
authResult = await makeAuthRequest({
twa_data: WebApp.initData,
ton_proof: {
account: tonConnectUI.wallet!.account,
ton_proof: tonProof,
},
});
} else {
// Case 3: Connected without proof - already authenticated
console.log('DEBUG: Connected without proof, proceeding without it');
authResult = await makeAuthRequest({
twa_data: WebApp.initData,
});
}
if (tonConnectUI.wallet?.account?.address) {
console.log('DEBUG: Selecting wallet', tonConnectUI.wallet.account.address);
await makeSelectWalletRequest({ wallet_address: tonConnectUI.wallet.account.address });
}
return authResult;
// Commented this part for two reasons:
// 1) When we include ton_proof from the wallet it fails the call for a reason of bad ton_proof
// 2) This call could happen only if the first case happened and it means that the ton_proof is already have been stored once before
// Case 2: Connected with proof - use it
// if (wallet?.connectItems?.tonProof && !("error" in wallet.connectItems.tonProof)) {
// console.log("DEBUG: Using existing proof", wallet.connectItems.tonProof.proof);
// return makeAuthRequest({
// twa_data: WebApp.initData,
// ton_proof: {
// account: wallet.account,
// ton_proof: wallet.connectItems.tonProof.proof,
// },
// });
// }
}); });
}); };
};

View File

@ -14,6 +14,7 @@ type UseCreateNewContentPayload = {
allowResale: boolean; allowResale: boolean;
authors: string[]; authors: string[];
royaltyParams: Royalty[]; royaltyParams: Royalty[];
downloadable: boolean;
}; };
export const useCreateNewContent = () => { export const useCreateNewContent = () => {

View File

@ -24,6 +24,9 @@ type RootStore = {
allowCover: boolean; allowCover: boolean;
setAllowCover: (allowCover: boolean) => void; setAllowCover: (allowCover: boolean) => void;
allowDwnld: boolean;
setAllowDwnld: (allowDwnld: boolean) => void;
cover: File | null; cover: File | null;
setCover: (cover: File | null) => void; setCover: (cover: File | null) => void;
@ -68,6 +71,9 @@ export const useRootStore = create<RootStore>((set) => ({
allowCover: false, allowCover: false,
setAllowCover: (allowCover) => set({ allowCover }), setAllowCover: (allowCover) => set({ allowCover }),
allowDwnld: false,
setAllowDwnld: (allowDwnld) => set({ allowDwnld }),
cover: null, cover: null,
setCover: (cover) => set({ cover }), setCover: (cover) => set({ cover }),