diff --git a/packages/atlas/src/CommonProviders.tsx b/packages/atlas/src/CommonProviders.tsx index 775adce9f5..d9e64afc66 100644 --- a/packages/atlas/src/CommonProviders.tsx +++ b/packages/atlas/src/CommonProviders.tsx @@ -34,30 +34,30 @@ export const CommonProviders: FC = ({ children }) => { return ( <> - - - - - - - - - - + + + + + + + + + + {children} - - - - - - - - - - + + + + + + + + + + ) } diff --git a/packages/atlas/src/api/hooks/dataObject.ts b/packages/atlas/src/api/hooks/dataObject.ts index 5aec867922..434f84c644 100644 --- a/packages/atlas/src/api/hooks/dataObject.ts +++ b/packages/atlas/src/api/hooks/dataObject.ts @@ -11,7 +11,7 @@ export const useDataObjectsAvailabilityLazy = (opts?: QueryHookOptions { - getDataObjectsAvailability({ + return getDataObjectsAvailability({ variables: { id_in: ids, }, diff --git a/packages/atlas/src/api/queries/__generated__/channels.generated.tsx b/packages/atlas/src/api/queries/__generated__/channels.generated.tsx index 9550fb624e..71cbbcf718 100644 --- a/packages/atlas/src/api/queries/__generated__/channels.generated.tsx +++ b/packages/atlas/src/api/queries/__generated__/channels.generated.tsx @@ -636,7 +636,21 @@ export type GetChannelPaymentEventsQuery = { | { __typename: 'CreatorTokenMarketBurnEventData' } | { __typename: 'CreatorTokenMarketMintEventData' } | { __typename: 'CreatorTokenMarketStartedEventData' } - | { __typename: 'CreatorTokenRevenueSplitIssuedEventData' } + | { + __typename: 'CreatorTokenRevenueSplitIssuedEventData' + token?: { __typename?: 'CreatorToken'; id: string; revenueShareRatioPermill: number } | null + revenueShare?: { + __typename?: 'RevenueShare' + id: string + allocation: string + startingAt: number + stakers: Array<{ + __typename?: 'RevenueShareParticipation' + earnings: string + account: { __typename?: 'TokenAccount'; member: { __typename?: 'Membership'; id: string } } + }> + } | null + } | { __typename: 'CreatorTokenSaleMintEventData' } | { __typename: 'CreatorTokenSaleStartedEventData' } | { @@ -1513,6 +1527,12 @@ export const GetChannelPaymentEventsDocument = gql` } } { data: { isTypeOf_in: ["ChannelPaymentMadeEventData"], payeeChannel: { id_eq: $channelId } } } + { + data: { + isTypeOf_in: ["CreatorTokenRevenueSplitIssuedEventData"] + token: { channel: { channel: { id_eq: $channelId } } } + } + } ] } ) { @@ -1639,6 +1659,25 @@ export const GetChannelPaymentEventsDocument = gql` controllerAccount } } + ... on CreatorTokenRevenueSplitIssuedEventData { + token { + id + revenueShareRatioPermill + } + revenueShare { + id + allocation + startingAt + stakers { + account { + member { + id + } + } + earnings + } + } + } } } } diff --git a/packages/atlas/src/api/queries/channels.graphql b/packages/atlas/src/api/queries/channels.graphql index 94a33f84fe..8ba57a38d7 100644 --- a/packages/atlas/src/api/queries/channels.graphql +++ b/packages/atlas/src/api/queries/channels.graphql @@ -163,6 +163,12 @@ query GetChannelPaymentEvents($channelId: String) { } } { data: { isTypeOf_in: ["ChannelPaymentMadeEventData"], payeeChannel: { id_eq: $channelId } } } + { + data: { + isTypeOf_in: ["CreatorTokenRevenueSplitIssuedEventData"] + token: { channel: { channel: { id_eq: $channelId } } } + } + } ] } ) { @@ -290,6 +296,25 @@ query GetChannelPaymentEvents($channelId: String) { controllerAccount } } + ... on CreatorTokenRevenueSplitIssuedEventData { + token { + id + revenueShareRatioPermill + } + revenueShare { + id + allocation + startingAt + stakers { + account { + member { + id + } + } + earnings + } + } + } } } } diff --git a/packages/atlas/src/components/CrtPreviewLayout/CrtPreviewLayout.tsx b/packages/atlas/src/components/CrtPreviewLayout/CrtPreviewLayout.tsx index 2c8e527068..8e8e615068 100644 --- a/packages/atlas/src/components/CrtPreviewLayout/CrtPreviewLayout.tsx +++ b/packages/atlas/src/components/CrtPreviewLayout/CrtPreviewLayout.tsx @@ -150,6 +150,7 @@ export const CrtPreviewLayout = ({ diff --git a/packages/atlas/src/components/Table/Table.styles.ts b/packages/atlas/src/components/Table/Table.styles.ts index ae80ec09d4..c87377ba1a 100644 --- a/packages/atlas/src/components/Table/Table.styles.ts +++ b/packages/atlas/src/components/Table/Table.styles.ts @@ -8,6 +8,12 @@ import { cVar, sizes } from '@/styles' export const Wrapper = styled.div` background-color: ${cVar('colorBackgroundMuted')}; overflow: auto; + + * { + .pointer { + cursor: pointer; + } + } ` export const TableBase = styled.table` diff --git a/packages/atlas/src/components/TablePaymentsHistory/TablePaymentsHistory.tsx b/packages/atlas/src/components/TablePaymentsHistory/TablePaymentsHistory.tsx index 0c86c02bd0..fa9dc31e17 100644 --- a/packages/atlas/src/components/TablePaymentsHistory/TablePaymentsHistory.tsx +++ b/packages/atlas/src/components/TablePaymentsHistory/TablePaymentsHistory.tsx @@ -10,6 +10,7 @@ import { TextButton } from '@/components/_buttons/Button' import { DialogModal } from '@/components/_overlays/DialogModal' import { absoluteRoutes } from '@/config/routes' import { getMemberAvatar } from '@/providers/assets/assets.helpers' +import { useUser } from '@/providers/user/user.hooks' import { SentryLogger } from '@/utils/logs' import { shortenString } from '@/utils/misc' @@ -112,6 +113,7 @@ const Sender = ({ sender }: { sender: PaymentHistory['sender'] }) => { skip: sender === 'council', } ) + const { activeChannel } = useUser() const member = memberships?.find((member) => member.controllerAccount === sender) const { urls: avatarUrls, isLoadingAsset: avatarLoading } = getMemberAvatar(member) @@ -128,6 +130,17 @@ const Sender = ({ sender }: { sender: PaymentHistory['sender'] }) => { /> ) } + + if (sender === 'own-channel') { + return ( + } + label="Own channel" + isInteractive={false} + /> + ) + } + if (member) { return ( diff --git a/packages/atlas/src/components/_crt/AllTokensSection/AllTokensSection.tsx b/packages/atlas/src/components/_crt/AllTokensSection/AllTokensSection.tsx index 95a02d5870..3dfa3ba339 100644 --- a/packages/atlas/src/components/_crt/AllTokensSection/AllTokensSection.tsx +++ b/packages/atlas/src/components/_crt/AllTokensSection/AllTokensSection.tsx @@ -31,7 +31,7 @@ export const AllTokensSection = () => { createdAt: new Date(createdAt), totalRevenue: 0, holdersNum: accountsNum, - isVerified: true, + isVerified: false, marketCap: lastPrice && totalSupply ? hapiBnToTokenNumber(new BN(lastPrice).mul(new BN(totalSupply))) ?? 0 : 0, status, channelId: channel?.channel.id ?? '', diff --git a/packages/atlas/src/components/_crt/AmmModalTemplates/AmmModalFormTemplate.tsx b/packages/atlas/src/components/_crt/AmmModalTemplates/AmmModalFormTemplate.tsx index 9932e7046e..9e575f25e2 100644 --- a/packages/atlas/src/components/_crt/AmmModalTemplates/AmmModalFormTemplate.tsx +++ b/packages/atlas/src/components/_crt/AmmModalTemplates/AmmModalFormTemplate.tsx @@ -20,6 +20,7 @@ type AmmModalFormTemplateProps = { tooltipText?: string }[] error?: string + maxInputValue?: number showTresholdButtons?: boolean } @@ -31,6 +32,7 @@ export const AmmModalFormTemplate = ({ control, error, showTresholdButtons, + maxInputValue, }: AmmModalFormTemplateProps) => { const { convertTokensToUSD } = useTokenPrice() @@ -52,6 +54,7 @@ export const AmmModalFormTemplate = ({ value={field.value} onChange={(value) => field.onChange(value ? Math.round(value) : value)} placeholder="0" + maxValue={maxInputValue} nodeEnd={ pricePerUnit ? ( diff --git a/packages/atlas/src/components/_crt/BuyFromMarketButton/BuyFromMarketButton.tsx b/packages/atlas/src/components/_crt/BuyFromMarketButton/BuyFromMarketButton.tsx index 2edf119832..e0d56c6a94 100644 --- a/packages/atlas/src/components/_crt/BuyFromMarketButton/BuyFromMarketButton.tsx +++ b/packages/atlas/src/components/_crt/BuyFromMarketButton/BuyFromMarketButton.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' +import { ProtectedActionWrapper } from '@/components/_auth/ProtectedActionWrapper' import { Button } from '@/components/_buttons/Button' import { BuyMarketTokenModal } from '@/components/_crt/BuyMarketTokenModal' @@ -12,9 +13,11 @@ export const BuyFromMarketButton = ({ tokenId }: BuyFromMarketButtonProps) => { return ( <> setShowModal(false)} /> - + + + ) } diff --git a/packages/atlas/src/components/_crt/BuyMarketTokenModal/BuyMarketTokenModal.tsx b/packages/atlas/src/components/_crt/BuyMarketTokenModal/BuyMarketTokenModal.tsx index 16e023d54b..b732b89a63 100644 --- a/packages/atlas/src/components/_crt/BuyMarketTokenModal/BuyMarketTokenModal.tsx +++ b/packages/atlas/src/components/_crt/BuyMarketTokenModal/BuyMarketTokenModal.tsx @@ -61,6 +61,7 @@ export const BuyMarketTokenModal = ({ tokenId, onClose: _onClose, show }: BuySal SentryLogger.error('Error while fetching creator token', 'BuyMarketTokenModal', error) }, }) + const hasActiveRevenueShare = data?.creatorTokenById?.revenueShares.some((rS) => !rS.finalized) const { data: memberTokenAccount } = useGetCreatorTokenHoldersQuery({ variables: { where: { @@ -172,8 +173,8 @@ export const BuyMarketTokenModal = ({ tokenId, onClose: _onClose, show }: BuySal data?.creatorTokenById?.id ?? 'N/A', data?.creatorTokenById?.symbol ?? 'N/A', channelId ?? 'N/A', - String(tokenAmount), - String(priceForAllToken) + tokenAmount, + priceForAllToken ) displaySnackbar({ iconType: 'success', @@ -302,7 +303,8 @@ export const BuyMarketTokenModal = ({ tokenId, onClose: _onClose, show }: BuySal withDenomination="before" /> ), - tooltipText: 'Price for a single token divided by the token amount.', + tooltipText: + 'Price of each incremental unit purchased or sold depends on overall quantity of tokens transacted, the actual average price per unit for the entire purchase or sale will differ from the price displayed for the first unit transacted.', }, { title: 'Fee', @@ -344,11 +346,19 @@ export const BuyMarketTokenModal = ({ tokenId, onClose: _onClose, show }: BuySal if (activeStep === BUY_MARKET_TOKEN_STEPS.form) { setPrimaryButtonProps({ text: 'Continue', - onClick: () => + onClick: () => { + if (hasActiveRevenueShare) { + displaySnackbar({ + iconType: 'error', + title: 'You cannot trade tokens during revenue share.', + }) + return + } handleSubmit((data) => { amountRef.current = data.tokenAmount setActiveStep(BUY_MARKET_TOKEN_STEPS.conditions) - })(), + })() + }, }) } @@ -358,7 +368,14 @@ export const BuyMarketTokenModal = ({ tokenId, onClose: _onClose, show }: BuySal onClick: onTransactionSubmit, }) } - }, [activeStep, data?.creatorTokenById?.symbol, handleSubmit, onTransactionSubmit]) + }, [ + activeStep, + data?.creatorTokenById?.symbol, + displaySnackbar, + handleSubmit, + hasActiveRevenueShare, + onTransactionSubmit, + ]) if (!loading && !currentAmm && show) { SentryLogger.error('BuyAmmModal invoked on token without active amm', 'BuyMarketTokenModal', { @@ -386,7 +403,7 @@ export const BuyMarketTokenModal = ({ tokenId, onClose: _onClose, show }: BuySal control={control} details={formDetails} pricePerUnit={pricePerUnit} - maxValue={10_000_000_000} + maxInputValue={10_000_000} error={formState.errors.tokenAmount?.message} validation={(value) => { if (!value || value < 1) return 'You need to buy at least one token' diff --git a/packages/atlas/src/components/_crt/ClaimRevenueShareButton/ClaimRevenueShareButton.tsx b/packages/atlas/src/components/_crt/ClaimRevenueShareButton/ClaimRevenueShareButton.tsx new file mode 100644 index 0000000000..0350828720 --- /dev/null +++ b/packages/atlas/src/components/_crt/ClaimRevenueShareButton/ClaimRevenueShareButton.tsx @@ -0,0 +1,39 @@ +import { useState } from 'react' + +import { FullCreatorTokenFragment } from '@/api/queries/__generated__/fragments.generated' +import { Button, ButtonProps } from '@/components/_buttons/Button' +import { ClaimShareModal } from '@/components/_crt/ClaimShareModal' +import { useSnackbar } from '@/providers/snackbars' + +export type ClaimRevenueShareButtonProps = { + token: FullCreatorTokenFragment + disabled?: boolean +} & Pick + +export const ClaimRevenueShareButton = ({ token, ...btnProps }: ClaimRevenueShareButtonProps) => { + const [openClaimShareModal, setOpenClaimShareModal] = useState(false) + const { displaySnackbar } = useSnackbar() + const hasActiveRevenueShare = token.revenueShares.some((revenueShare) => !revenueShare.finalized) + + return ( + <> + + setOpenClaimShareModal(false)} show={openClaimShareModal} tokenId={token.id} /> + + ) +} diff --git a/packages/atlas/src/components/_crt/ClaimShareModal/ClaimShareModal.tsx b/packages/atlas/src/components/_crt/ClaimShareModal/ClaimShareModal.tsx index 7fe2acfc3e..8b40f389d9 100644 --- a/packages/atlas/src/components/_crt/ClaimShareModal/ClaimShareModal.tsx +++ b/packages/atlas/src/components/_crt/ClaimShareModal/ClaimShareModal.tsx @@ -1,9 +1,12 @@ import BN from 'bn.js' +import { useEffect } from 'react' import { + useGetCreatorTokenHoldersQuery, useGetFullCreatorTokenQuery, useGetRevenueShareDividendQuery, } from '@/api/queries/__generated__/creatorTokens.generated' +import { FullCreatorTokenFragment } from '@/api/queries/__generated__/fragments.generated' import { SvgAlertsInformative24 } from '@/assets/icons' import { Banner } from '@/components/Banner' import { FlexBox } from '@/components/FlexBox' @@ -13,7 +16,6 @@ import { SkeletonLoader } from '@/components/_loaders/SkeletonLoader' import { DialogModal } from '@/components/_overlays/DialogModal' import { atlasConfig } from '@/config' import { useBlockTimeEstimation } from '@/hooks/useBlockTimeEstimation' -import { useGetTokenBalance } from '@/hooks/useGetTokenBalance' import { hapiBnToTokenNumber } from '@/joystream-lib/utils' import { useFee, useJoystream } from '@/providers/joystream' import { useNetworkUtils } from '@/providers/networkUtils/networkUtils.hooks' @@ -27,36 +29,64 @@ type ClaimShareModalProps = { show?: boolean onClose: () => void tokenId?: string + token?: FullCreatorTokenFragment } -export const ClaimShareModal = ({ onClose, show, tokenId }: ClaimShareModalProps) => { - const { data } = useGetFullCreatorTokenQuery({ variables: { id: tokenId ?? '' }, skip: !tokenId }) +export const ClaimShareModal = ({ onClose, show, tokenId: _tokenId, token: _token }: ClaimShareModalProps) => { + const { data, refetch } = useGetFullCreatorTokenQuery({ + variables: { id: _tokenId ?? '' }, + skip: !_tokenId || !!_token, + notifyOnNetworkStatusChange: true, + }) const { refetchAllMemberTokenBalanceData, refetchCreatorTokenData } = useNetworkUtils() - const token = data?.creatorTokenById + const token = _token ?? data?.creatorTokenById + const tokenId = _token?.id ?? _tokenId + const tokenName = token?.symbol ?? 'N/A' const { joystream, proxyCallback } = useJoystream() const { memberId } = useUser() const { displaySnackbar } = useSnackbar() const handleTransaction = useTransaction() - const { tokenBalance } = useGetTokenBalance(token?.id, memberId ?? '') const { fullFee } = useFee('participateInSplitTx') const activeRevenueShare = token?.revenueShares.find((rS) => !rS.finalized) const { convertBlockToMsTimestamp } = useBlockTimeEstimation() + + useEffect(() => { + if (show) { + refetch() + } + }, [refetch, show]) + + const { data: holderData } = useGetCreatorTokenHoldersQuery({ + variables: { + where: { + token: { + id_eq: tokenId, + }, + member: { + id_eq: memberId, + }, + }, + }, + skip: !memberId || !tokenId, + }) + const { totalAmount, stakedAmount } = holderData?.tokenAccounts[0] ?? {} + const stakableBalance = totalAmount && stakedAmount ? +totalAmount - +stakedAmount : 0 const { data: dividendData, loading: loadingDividendData } = useGetRevenueShareDividendQuery({ variables: { tokenId: token?.id ?? '', - stakingAmount: tokenBalance, + stakingAmount: stakableBalance, }, - skip: !tokenBalance || !token, + skip: !stakableBalance || !token, }) const onSubmit = async () => { - if (!joystream || !token || !memberId || !tokenBalance || !activeRevenueShare) { + if (!joystream || !token || !memberId || !stakableBalance || !activeRevenueShare) { SentryLogger.error('Failed to submit claim share transaction', 'ClaimShareModal', { joystream, token, memberId, - tokenBalance, + stakableBalance, activeRevenueShare, }) return @@ -66,7 +96,7 @@ export const ClaimShareModal = ({ onClose, show, tokenId }: ClaimShareModalProps (await joystream.extrinsics).participateInSplit( token.id, memberId, - String(tokenBalance), + String(stakableBalance), proxyCallback(updateStatus) ), fee: fullFee, @@ -89,7 +119,7 @@ export const ClaimShareModal = ({ onClose, show, tokenId }: ClaimShareModalProps joystream, token, memberId, - tokenBalance, + stakableBalance, activeRevenueShare, }) displaySnackbar({ @@ -127,7 +157,7 @@ export const ClaimShareModal = ({ onClose, show, tokenId }: ClaimShareModalProps You will lock export const CloseRevenueShareButton = ({ variant, disabled, hideOnInactiveRevenue, - tokenId, revenueShareEndingBlock, + token, }: CloseRevenueShareButtonProps) => { const { joystream, proxyCallback } = useJoystream() const { channelId, memberId } = useUser() @@ -31,6 +33,7 @@ export const CloseRevenueShareButton = ({ const { displaySnackbar } = useSnackbar() const { currentBlock } = useJoystreamStore() const { refetchCreatorTokenData } = useNetworkUtils() + const { trackRevenueShareClosed } = useSegmentAnalytics() const finalizeRevenueShare = useCallback(() => { if (!joystream || !memberId || !channelId) { @@ -45,7 +48,9 @@ export const CloseRevenueShareButton = ({ txFactory: async (updateStatus) => (await joystream.extrinsics).finalizeRevenueSplit(memberId, channelId, proxyCallback(updateStatus)), onTxSync: async (data) => { - refetchCreatorTokenData(tokenId) + refetchCreatorTokenData(token?.id ?? '') + trackRevenueShareClosed(channelId, token?.id || 'N/A', token?.symbol || 'N/A') + displaySnackbar({ title: 'Revenue share is closed', description: `Remaining unclaimed ${hapiBnToTokenNumber(new BN(data.amount))} ${ @@ -74,7 +79,9 @@ export const CloseRevenueShareButton = ({ memberId, proxyCallback, refetchCreatorTokenData, - tokenId, + token?.id, + token?.symbol, + trackRevenueShareClosed, ]) if (hideOnInactiveRevenue && currentBlock < (revenueShareEndingBlock ?? 0)) { diff --git a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/SetupTokenStep.tsx b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/SetupTokenStep.tsx index 81c2c9c173..fe78aab215 100644 --- a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/SetupTokenStep.tsx +++ b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/SetupTokenStep.tsx @@ -188,6 +188,7 @@ export const SetupTokenStep = ({ setPrimaryButtonProps, onSubmit, form, setPrevi render={({ field }) => ( diff --git a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.utils.ts b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.utils.ts index 8f3a9261c9..63190280bb 100644 --- a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.utils.ts +++ b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.utils.ts @@ -73,19 +73,20 @@ export const createTokenIssuanceSchema = (tokenName: string) => .min(3_000, `Can’t issue less than 3 000 $${tokenName}.`) .max(100_000, `Can’t issue more than 100 000 $${tokenName}.`), assuranceType: z.enum(['safe', 'risky', 'secure', 'custom']), - cliff: z.enum(['0', '1', '3', '6']).nullable(), - vesting: z.enum(['0', '1', '3', '6']).nullable(), + cliff: z.string().nullable(), + vesting: z.string().nullable(), firstPayout: z .number() - .positive('Payout cannot be a negative number.') + .min(0, 'Payout cannot be a negative number.') .max(100, 'Payout cannot exceed 100%.') .optional(), }) .refine( (data) => { - if (['1', '3', '6'].includes(data.vesting ?? '')) { - return !!data.firstPayout + if (data.vesting) { + return data.firstPayout !== undefined } + return true }, { @@ -102,7 +103,7 @@ export const createTokenIssuanceSchema = (tokenName: string) => }, { path: ['cliff'], - message: 'Select cliff for your token.', + message: 'Select locked period for your token.', } ) .refine( diff --git a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenSummaryStep.tsx b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenSummaryStep.tsx index 8d2d38d6d6..10d8e3ba83 100644 --- a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenSummaryStep.tsx +++ b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenSummaryStep.tsx @@ -85,13 +85,7 @@ export const TokenSummaryStep = ({ setPrimaryButtonProps, form, onSuccess }: Tok proxyCallback(handleUpdate) ), onTxSync: async ({ tokenId }) => { - trackTokenMintingCompleted( - channelId, - tokenId, - form.name, - String(form.creatorIssueAmount ?? 0), - form.assuranceType - ) + trackTokenMintingCompleted(channelId, tokenId, form.name, form.creatorIssueAmount ?? 0, form.assuranceType) onSuccess() }, snackbarSuccessMessage: { @@ -188,7 +182,7 @@ export const TokenSummaryStep = ({ setPrimaryButtonProps, form, onSuccess }: Tok tooltipText="This signals to your investors that they can be 100% sure the token price on the market during this period will be exclusively impacted by market conditions. This is a strong signal of integrity." > - {pluralizeNoun(Number(cliff), 'month')} + {Number(cliff) > 0 ? pluralizeNoun(Number(cliff), 'month') : 'No locked period'} {/*{cliffBanner}*/} @@ -204,7 +198,7 @@ export const TokenSummaryStep = ({ setPrimaryButtonProps, form, onSuccess }: Tok )} - {!!(form.creatorIssueAmount && payout) && ( + {!!(form.creatorIssueAmount && payout !== undefined) && ( ({ })) const COLUMNS: TableProps['columns'] = [ - { Header: 'Member', accessor: 'member', width: 3 }, + { Header: 'Member', accessor: 'member', width: 4 }, { Header: 'Total', accessor: 'total', width: 2 }, - { Header: 'Vested', accessor: 'vested', width: 1 }, + { Header: 'Unlocked', accessor: 'transferable', width: 3 }, ] type CrtHolder = { memberId: string total: number | BN - vested: number | BN + tokenSymbol: string + tokenId: string allocation: number } @@ -70,13 +72,20 @@ export const CrtHoldersTable = ({ ), total: ( - - + + ({row.allocation}%) ), - vested: , + transferable: ( + + ), })), [data, ownerId] ) @@ -86,6 +95,7 @@ export const CrtHoldersTable = ({ onRowClick={(rowIdx) => { navigate(absoluteRoutes.viewer.memberById(data[rowIdx].memberId)) }} + minWidth={350} columns={COLUMNS} data={isLoading ? tableLoadingData : mappedData} className={className} @@ -95,6 +105,11 @@ export const CrtHoldersTable = ({ ) } +const StyledTransferableBalance = styled(TransferableBalance)` + text-align: right; + white-space: nowrap; +` + export const StyledTable = styled(Table)` tr { cursor: pointer; diff --git a/packages/atlas/src/components/_crt/CrtPortfolioTable/CrtPortfolioTable.tsx b/packages/atlas/src/components/_crt/CrtPortfolioTable/CrtPortfolioTable.tsx index c644d42556..0f9b473a8a 100644 --- a/packages/atlas/src/components/_crt/CrtPortfolioTable/CrtPortfolioTable.tsx +++ b/packages/atlas/src/components/_crt/CrtPortfolioTable/CrtPortfolioTable.tsx @@ -20,7 +20,7 @@ import { FlexBox } from '@/components/FlexBox' import { NumberFormat } from '@/components/NumberFormat' import { Table, TableProps } from '@/components/Table' import { ColumnBox } from '@/components/Table/Table.styles' -import { Text } from '@/components/Text' +import { Text, TextVariant } from '@/components/Text' import { Button } from '@/components/_buttons/Button' import { BuyMarketTokenModal } from '@/components/_crt/BuyMarketTokenModal' import { SellTokenModal } from '@/components/_crt/SellTokenModal' @@ -48,7 +48,7 @@ const COLUMNS: TableProps['columns'] = [ { Header: 'Token', accessor: 'token', width: 150 }, { Header: 'Status', accessor: 'status', width: 200 }, { Header: 'Transferable', accessor: 'transferable', width: 100 }, - { Header: 'Vested', accessor: 'vested', width: 100 }, + { Header: 'Staked', accessor: 'staked', width: 100 }, { Header: 'Total', accessor: 'total', width: 100 }, { Header: '', accessor: 'utils', width: 70 }, ] @@ -58,7 +58,7 @@ export type PortfolioToken = { tokenName: string isVerified: boolean status: TokenStatus - vested: number + staked: number total: number tokenId: string memberId: string @@ -89,10 +89,10 @@ export const CrtPortfolioTable = ({ data, emptyState, isLoading }: CrtPortfolioT ), - vested: ( + staked: ( {row.hasStaked && } - + ), total: ( @@ -163,7 +163,11 @@ export const TokenInfo = ({ /> )} - + (channelId ? navigate(absoluteRoutes.viewer.channel(channelId, { tab: 'Token' })) : undefined)} + className="pointer" + alignItems="center" + > {tokenTitle} @@ -262,13 +266,26 @@ export const TransferableBalance = ({ memberId, tokenId, ticker, + className, + variant, }: { memberId: string tokenId: string ticker?: string + className?: string + variant?: TextVariant }) => { const { tokenBalance } = useGetTokenBalance(tokenId, memberId) - return + return ( + + ) } const StyledTable = styled(Table)<{ isEmpty?: boolean }>` diff --git a/packages/atlas/src/components/_crt/CrtRevenueShareWidget/CrtRevenueShareWidget.tsx b/packages/atlas/src/components/_crt/CrtRevenueShareWidget/CrtRevenueShareWidget.tsx index c834fb4515..10f45b8a2b 100644 --- a/packages/atlas/src/components/_crt/CrtRevenueShareWidget/CrtRevenueShareWidget.tsx +++ b/packages/atlas/src/components/_crt/CrtRevenueShareWidget/CrtRevenueShareWidget.tsx @@ -120,7 +120,7 @@ export const CrtRevenueShareWidget = ({ token, onTabSwitch }: CrtHoldersWidgetPr {activeRevenueShare ? ( - + ) : null} diff --git a/packages/atlas/src/components/_crt/CrtStatusWidget/CrtStatusWidget.tsx b/packages/atlas/src/components/_crt/CrtStatusWidget/CrtStatusWidget.tsx index 015f3686db..5fc71d61c9 100644 --- a/packages/atlas/src/components/_crt/CrtStatusWidget/CrtStatusWidget.tsx +++ b/packages/atlas/src/components/_crt/CrtStatusWidget/CrtStatusWidget.tsx @@ -85,7 +85,9 @@ export const CrtStatusWidget: FC = ({ token }) => { avoidIconStyling tileSize={smMatch ? 'big' : 'bigSmall'} caption="Total revenue Shares" - content={data?.getCumulativeHistoricalShareAllocation.cumulativeHistoricalAllocation ?? 0} + content={hapiBnToTokenNumber( + new BN(data?.getCumulativeHistoricalShareAllocation.cumulativeHistoricalAllocation ?? 0) + )} icon={} withDenomination /> @@ -118,7 +120,7 @@ const MarketDetails = ({ token }: { token: FullCreatorTokenFragment }) => { (amount: number) => { const currentAmm = token?.ammCurves.find((amm) => !amm.finalized) return calcBuyMarketPricePerToken( - currentAmm?.mintedByAmm, + currentAmm ? +currentAmm?.mintedByAmm - +currentAmm?.burnedByAmm : 0, currentAmm?.ammSlopeParameter, currentAmm?.ammInitPrice, amount @@ -129,12 +131,12 @@ const MarketDetails = ({ token }: { token: FullCreatorTokenFragment }) => { return ( } withDenomination tileSize="big" - tooltipText="Price per unit is calculated for current market supply and can quickly change." + tooltipText="Price of each incremental unit purchased or sold depends on overall quantity of tokens transacted, the actual average price per unit for the entire purchase or sale will differ from the price displayed for the first unit transacted." /> diff --git a/packages/atlas/src/components/_crt/HoldersWidget/HoldersWidget.tsx b/packages/atlas/src/components/_crt/HoldersWidget/HoldersWidget.tsx index 4917e88f1a..194ccfdceb 100644 --- a/packages/atlas/src/components/_crt/HoldersWidget/HoldersWidget.tsx +++ b/packages/atlas/src/components/_crt/HoldersWidget/HoldersWidget.tsx @@ -14,13 +14,14 @@ import { cVar, sizes } from '@/styles' export type HoldersWidgetProps = { ownerId: string tokenId: string + tokenSymbol: string totalSupply: number totalHolders: number } const TILES_PER_PAGE = 5 -export const HoldersWidget = ({ tokenId, ownerId, totalSupply, totalHolders }: HoldersWidgetProps) => { +export const HoldersWidget = ({ tokenId, ownerId, totalSupply, totalHolders, tokenSymbol }: HoldersWidgetProps) => { const [showModal, setShowModal] = useState(false) const { holders: _holders, @@ -38,9 +39,10 @@ export const HoldersWidget = ({ tokenId, ownerId, totalSupply, totalHolders }: H memberId: holder?.member?.id ?? '', total: +holder.totalAmount, allocation: totalSupply ? +formatNumberShort((+holder.totalAmount / totalSupply) * 100) : 0, - vested: +(holder.vestingSchedules[0]?.totalVestingAmount ?? 0), + tokenId, + tokenSymbol: tokenSymbol, })) ?? [], - [_holders, totalSupply] + [_holders, tokenId, tokenSymbol, totalSupply] ) const [firstPageHolders, setFirstPageHolders] = useState([]) diff --git a/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.tsx b/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.tsx index db7c74af75..74022f4c07 100644 --- a/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.tsx +++ b/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.tsx @@ -111,7 +111,7 @@ export const MarketDrawer = ({ show, onClose, tokenId }: CrtMarketSaleViewProps) icon: , }, { - text: "You are no earning royalties from other people's transactions on your token.", + text: "You are not earning royalties from other people's transactions on your token.", icon: , }, { diff --git a/packages/atlas/src/components/_crt/MarketplaceCrtTable/MarketplaceCrtTable.tsx b/packages/atlas/src/components/_crt/MarketplaceCrtTable/MarketplaceCrtTable.tsx index e47390f06a..5f7a995b0c 100644 --- a/packages/atlas/src/components/_crt/MarketplaceCrtTable/MarketplaceCrtTable.tsx +++ b/packages/atlas/src/components/_crt/MarketplaceCrtTable/MarketplaceCrtTable.tsx @@ -1,6 +1,5 @@ import styled from '@emotion/styled' import { useMemo } from 'react' -import { useNavigate } from 'react-router' import { TokenStatus } from '@/api/queries/__generated__/baseTypes.generated' import { JoyTokenIcon } from '@/components/JoyTokenIcon' @@ -9,7 +8,6 @@ import { Table, TableProps } from '@/components/Table' import { ColumnBox } from '@/components/Table/Table.styles' import { Text } from '@/components/Text' import { SkeletonLoader } from '@/components/_loaders/SkeletonLoader' -import { absoluteRoutes } from '@/config/routes' import { cVar } from '@/styles' import { pluralizeNoun } from '@/utils/misc' import { formatDate } from '@/utils/time' @@ -64,7 +62,6 @@ export const MarketplaceCrtTable = ({ pagination, pageSize, }: MarketplaceCrtTableProps) => { - const navigate = useNavigate() const mappingData = useMemo(() => { return data.map((row) => ({ token: , @@ -103,9 +100,6 @@ export const MarketplaceCrtTable = ({ return ( <> { - navigate(absoluteRoutes.viewer.channel(data[rowIdx].channelId, { tab: 'Token' })) - }} minWidth={730} isEmpty={!mappingData.length} columns={COLUMNS} diff --git a/packages/atlas/src/components/_crt/OnboardingProgressModal/OnboardingProgressModal.tsx b/packages/atlas/src/components/_crt/OnboardingProgressModal/OnboardingProgressModal.tsx index 190dfcac89..3c38d1571c 100644 --- a/packages/atlas/src/components/_crt/OnboardingProgressModal/OnboardingProgressModal.tsx +++ b/packages/atlas/src/components/_crt/OnboardingProgressModal/OnboardingProgressModal.tsx @@ -9,6 +9,7 @@ import { IllustrationWrapper, LottieContainer, } from '@/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/SignUpSuccessStep.styles' +import { TextButton } from '@/components/_buttons/Button' import { DialogModal } from '@/components/_overlays/DialogModal' import { useMediaMatch } from '@/hooks/useMediaMatch' import { useMountEffect } from '@/hooks/useMountEffect' @@ -24,8 +25,15 @@ const data = { }, expert: { title: 'Congratulations on becoming a token expert!', - description: - 'Now when you know everything about managing your token you close “your progress” section and keep managing token on your own.', + // todo export discord link to config + description: ( + <> + Congratulations on becoming a token expert! You have qualified for the token expert role and your token can be + featured on the marketplace. Reach out in{' '} + Gleev Creator Discord to claim the token expert role + and token featuring! + + ), }, } diff --git a/packages/atlas/src/components/_crt/RevenueShareHistoryTable/RevenueShareHistoryTable.tsx b/packages/atlas/src/components/_crt/RevenueShareHistoryTable/RevenueShareHistoryTable.tsx index 9935402330..f182f3b9f6 100644 --- a/packages/atlas/src/components/_crt/RevenueShareHistoryTable/RevenueShareHistoryTable.tsx +++ b/packages/atlas/src/components/_crt/RevenueShareHistoryTable/RevenueShareHistoryTable.tsx @@ -38,12 +38,12 @@ export const RevenueShareHistoryTable = ({ data }: RevenueShareHistoryTableProps return { endDate: , participants: ( - + {row.stakers.length}/{potentialParticipants ?? 'N/A'} - {potentialParticipants ? `(${row.stakers.length / potentialParticipants}%)` : ''} + {potentialParticipants ? `(${(row.stakers.length / potentialParticipants) * 100}%)` : ''} - + ), total: , userClaimed: , @@ -56,6 +56,10 @@ export const RevenueShareHistoryTable = ({ data }: RevenueShareHistoryTableProps return } +const ParticipantsText = styled(Text)` + text-align: right; +` + const StyledTable = styled(Table)` th:not(:nth-child(1)) { justify-content: end; diff --git a/packages/atlas/src/components/_crt/RevenueShareModalButton/RevenueShareModalButton.tsx b/packages/atlas/src/components/_crt/RevenueShareModalButton/RevenueShareModalButton.tsx index 6f3baf815c..b590ab4493 100644 --- a/packages/atlas/src/components/_crt/RevenueShareModalButton/RevenueShareModalButton.tsx +++ b/packages/atlas/src/components/_crt/RevenueShareModalButton/RevenueShareModalButton.tsx @@ -14,15 +14,15 @@ export type RevenueShareModalButtonProps = { export const RevenueShareModalButton = ({ token, variant, disabled }: RevenueShareModalButtonProps) => { const [openRevenueShareModal, setOpenRevenueShareModal] = useState(false) const { displaySnackbar } = useSnackbar() - const hasOpenedMarket = !!token.currentAmmSale const hasOpenedRevenueShare = token.revenueShares.some((revenueShare) => !revenueShare.finalized) + return ( <> - ) + return case 'unlock': return ( - ) + return case 'unlock': return ( + {data?.creatorTokenById ? : null} ) case 'unlock': @@ -93,10 +107,6 @@ export const RevenueShareWidget = ({ tokenName, tokenId, revenueShare, memberId return ( <> - {openClaimShareModal && ( - setOpenClaimShareModal(false)} show={openClaimShareModal} tokenId={tokenId} /> - )} - @@ -121,7 +131,7 @@ export const RevenueShareWidget = ({ tokenName, tokenId, revenueShare, memberId { return ( <> setShowModal(false)} /> - + + + ) } diff --git a/packages/atlas/src/components/_crt/SellTokenModal/SellTokenModal.tsx b/packages/atlas/src/components/_crt/SellTokenModal/SellTokenModal.tsx index 9b0d0cc240..2327e4cde6 100644 --- a/packages/atlas/src/components/_crt/SellTokenModal/SellTokenModal.tsx +++ b/packages/atlas/src/components/_crt/SellTokenModal/SellTokenModal.tsx @@ -46,6 +46,7 @@ export const SellTokenModal = ({ tokenId, onClose: _onClose, show }: SellTokenMo SentryLogger.error('Failed to fetch token data', 'SellTokenModal', { error }) }, }) + const hasActiveRevenueShare = data?.creatorTokenById?.revenueShares.some((rS) => !rS.finalized) const currentAmm = data?.creatorTokenById?.currentAmmSale const ammBalance = currentAmm ? +currentAmm.mintedByAmm - +currentAmm.burnedByAmm : 0 @@ -166,7 +167,8 @@ export const SellTokenModal = ({ tokenId, onClose: _onClose, show }: SellTokenMo withDenomination="before" /> ), - tooltipText: 'Averaged price per token.', + tooltipText: + 'Price of each incremental unit purchased or sold depends on overall quantity of tokens transacted, the actual average price per unit for the entire purchase or sale will differ from the price displayed for the first unit transacted.', }, { title: 'Fee', @@ -190,10 +192,19 @@ export const SellTokenModal = ({ tokenId, onClose: _onClose, show }: SellTokenMo [calculateSlippageAmount, fullFee, pricePerUnit, title, tokenAmount] ) - const onFormSubmit = () => + const onFormSubmit = () => { + if (hasActiveRevenueShare) { + displaySnackbar({ + iconType: 'error', + title: 'You cannot trade tokens during revenue share.', + }) + return + } + handleSubmit(() => { setStep('summary') })() + } const onTransactionSubmit = async () => { const slippageTolerance = calculateSlippageAmount(tokenAmount) @@ -222,8 +233,8 @@ export const SellTokenModal = ({ tokenId, onClose: _onClose, show }: SellTokenMo tokenId, data?.creatorTokenById?.symbol ?? 'N/A', channelId ?? 'N/A', - tokenAmount.toString(), - joyAmountReceived.toString() + tokenAmount, + joyAmountReceived ) displaySnackbar({ iconType: 'success', diff --git a/packages/atlas/src/components/_crt/StartMarketModal/StartMarketModal.tsx b/packages/atlas/src/components/_crt/StartMarketModal/StartMarketModal.tsx index 7f3a691e1c..98d1fff669 100644 --- a/packages/atlas/src/components/_crt/StartMarketModal/StartMarketModal.tsx +++ b/packages/atlas/src/components/_crt/StartMarketModal/StartMarketModal.tsx @@ -114,7 +114,7 @@ export const StartMarketModal = ({ onClose, show, tokenId }: StartMarketModalPro icon: , }, { - text: "You are no earning royalties from other people's transactions on your token.", + text: "You are not earning royalties from other people's transactions on your token.", icon: , }, { diff --git a/packages/atlas/src/components/_crt/StartRevenueShareModal/StartRevenueShareModal.tsx b/packages/atlas/src/components/_crt/StartRevenueShareModal/StartRevenueShareModal.tsx index e304bf1557..e8584b8304 100644 --- a/packages/atlas/src/components/_crt/StartRevenueShareModal/StartRevenueShareModal.tsx +++ b/packages/atlas/src/components/_crt/StartRevenueShareModal/StartRevenueShareModal.tsx @@ -19,6 +19,7 @@ import { absoluteRoutes } from '@/config/routes' import { useBlockTimeEstimation } from '@/hooks/useBlockTimeEstimation' import { useClipboard } from '@/hooks/useClipboard' import { useGetTokenBalance } from '@/hooks/useGetTokenBalance' +import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics' import { useFee, useJoystream, useSubscribeAccountBalance } from '@/providers/joystream' import { useNetworkUtils } from '@/providers/networkUtils/networkUtils.hooks' import { useSnackbar } from '@/providers/snackbars' @@ -62,9 +63,11 @@ export const StartRevenueShare = ({ token, onClose, show }: StartRevenueSharePro variables: { id: token.id, }, + notifyOnNetworkStatusChange: true, fetchPolicy: 'no-cache', }) const { fullFee } = useFee('issueRevenueSplitTx', ['1', '1', 10000, 10000]) + const { trackRevenueShareStarted } = useSegmentAnalytics() const memoizedChannelStateBloatBond = useMemo(() => { return new BN(activeChannel?.channelStateBloatBond || 0) @@ -126,7 +129,11 @@ export const StartRevenueShare = ({ token, onClose, show }: StartRevenueSharePro : null if (typeof duration !== 'number' || duration < 0) { - displaySnackbar({ title: 'Failed to parse ending date', iconType: 'error', description: 'Please try again.' }) + displaySnackbar({ + title: duration && duration < 0 ? 'Revenue share cannot end in the past' : 'Failed to parse ending date', + iconType: 'error', + description: 'Please try again.', + }) return } @@ -145,6 +152,7 @@ export const StartRevenueShare = ({ token, onClose, show }: StartRevenueSharePro onClose() setShowSuccessModal(true) }) + trackRevenueShareStarted(channelId, token.id, token.symbol || 'N/A') }, onError: () => { displaySnackbar({ @@ -223,7 +231,7 @@ export const StartRevenueShare = ({ token, onClose, show }: StartRevenueSharePro }, { - title: 'Your holders will receive', + title: 'All holders can receive', content: ( , - tooltipText: 'Lorem ipsum', }, { title: 'You will receive', @@ -286,7 +293,7 @@ export const StartRevenueShare = ({ token, onClose, show }: StartRevenueSharePro refetchCreatorTokenData(localTokenData.creatorTokenById?.id ?? '') }} show={openClaimShareModal} - tokenId={localTokenData.creatorTokenById.id} + token={localTokenData.creatorTokenById} /> )} setShowMarketDrawer(false), []) const hasOpenedMarket = !!token.currentAmmSale - const hasOpenedRevenueShare = token.revenueShares.some((revenueShare) => !revenueShare.finalized) return ( <> @@ -33,14 +32,6 @@ export const StartSaleOrMarketButton = ({ token, ...buttonProps }: StartSaleOrMa return } - if (hasOpenedRevenueShare) { - displaySnackbar({ - title: 'You cannot start a market while the revenue share is active', - iconType: 'info', - }) - return - } - setShowChoiceDrawer(true) }} icon={} diff --git a/packages/atlas/src/components/_inputs/Slider/RatioSlider.tsx b/packages/atlas/src/components/_inputs/Slider/RatioSlider.tsx index d66284cc18..13f3e122e3 100644 --- a/packages/atlas/src/components/_inputs/Slider/RatioSlider.tsx +++ b/packages/atlas/src/components/_inputs/Slider/RatioSlider.tsx @@ -28,9 +28,9 @@ export const RatioSlider = forwardRef( : (evt) => onChange?.(Number(evt.target.value)), [controlledValue, onChange] ) - - const length = max - min - const valuePercent = `${(value / length) * 100}%` + const length = max + const numberOfSteps = length / step + const valuePercent = `${(value / step / numberOfSteps) * 100}%` const steps = useMemo(() => { const stepPercent = (step / length) * 100 @@ -58,7 +58,7 @@ export const RatioSlider = forwardRef( {steps.map((x, index) => { - const cls = Math.min(index * step, max) <= value ? 'active' : '' + const cls = Math.min(Math.max((index + 1) * step, min), max) <= value ? 'active' : '' return })} diff --git a/packages/atlas/src/components/_navigation/TopbarBase/TopbarBase.styles.ts b/packages/atlas/src/components/_navigation/TopbarBase/TopbarBase.styles.ts index 6a9b208915..4a3b4fc077 100644 --- a/packages/atlas/src/components/_navigation/TopbarBase/TopbarBase.styles.ts +++ b/packages/atlas/src/components/_navigation/TopbarBase/TopbarBase.styles.ts @@ -23,7 +23,7 @@ export const Header = styled.header` ${media.md} { display: grid; grid-template-rows: auto; - grid-template-columns: 1fr minmax(480px, 1fr) 1fr; + grid-template-columns: auto 1fr minmax(480px, 1fr) 1fr; padding: ${sizes(4)} ${sizes(8)} ${sizes(4)} calc(var(--size-sidenav-width-collapsed) + ${sizes(8)}); } ` @@ -38,6 +38,7 @@ export const LogoLink = styled(Link)` /* increase the clickable area */ padding: 16px; margin: -16px; + margin-right: ${sizes(-2)}; h4 { white-space: nowrap; diff --git a/packages/atlas/src/components/_navigation/TopbarBase/TopbarBase.tsx b/packages/atlas/src/components/_navigation/TopbarBase/TopbarBase.tsx index 593fd804cf..8fa9b6fd63 100644 --- a/packages/atlas/src/components/_navigation/TopbarBase/TopbarBase.tsx +++ b/packages/atlas/src/components/_navigation/TopbarBase/TopbarBase.tsx @@ -1,7 +1,8 @@ import { FC, PropsWithChildren, ReactNode } from 'react' import { AppLogo } from '@/components/AppLogo' -import { Text } from '@/components/Text' +import { FlexBox } from '@/components/FlexBox' +import { TextButton } from '@/components/_buttons/Button' import { useMediaMatch } from '@/hooks/useMediaMatch' import { Header, LogoDivider, LogoLink } from './TopbarBase.styles' @@ -23,14 +24,16 @@ export const TopbarBase: FC = ({ children, fullLogoNode, logoLi {!noLogo && ( {mdMatch ? fullLogoNode : } - {xsMatch && } - {xsMatch && ( - - {smMatch ? 'Powered' : ''} by Joystream - - )} )} + + {xsMatch && } + {xsMatch && ( + + {smMatch ? 'Powered' : ''} by Joystream + + )} + {children} ) diff --git a/packages/atlas/src/components/_overlays/MemberDropdown/MemberDropdownNav.tsx b/packages/atlas/src/components/_overlays/MemberDropdown/MemberDropdownNav.tsx index d3b6800751..c87e779e1c 100644 --- a/packages/atlas/src/components/_overlays/MemberDropdown/MemberDropdownNav.tsx +++ b/packages/atlas/src/components/_overlays/MemberDropdown/MemberDropdownNav.tsx @@ -189,7 +189,7 @@ export const MemberDropdownNav: FC = ({ diff --git a/packages/atlas/src/hooks/useGetTokenBalance.ts b/packages/atlas/src/hooks/useGetTokenBalance.ts index 840ba9b869..82eb49adc5 100644 --- a/packages/atlas/src/hooks/useGetTokenBalance.ts +++ b/packages/atlas/src/hooks/useGetTokenBalance.ts @@ -11,7 +11,7 @@ export const useGetTokenBalance = (tokenId?: string, memberId?: string) => { const [tokenBalance, setTokenBalance] = useState(null) const blockHeightRef = useRef(null) - const { loading } = useGetChannelTokenBalanceQuery({ + const { loading, refetch } = useGetChannelTokenBalanceQuery({ variables: { tokenId: tokenId ?? '', memberId: memberId ?? currentMemberId ?? '', @@ -38,5 +38,6 @@ export const useGetTokenBalance = (tokenId?: string, memberId?: string) => { return { tokenBalance: tokenBalance ?? 0, isLoading: loading, + refetch, } } diff --git a/packages/atlas/src/hooks/useSegmentAnalytics.ts b/packages/atlas/src/hooks/useSegmentAnalytics.ts index 2456771fb5..0eef4282ca 100644 --- a/packages/atlas/src/hooks/useSegmentAnalytics.ts +++ b/packages/atlas/src/hooks/useSegmentAnalytics.ts @@ -1,4 +1,5 @@ import { useCallback, useRef } from 'react' +import { useSearchParams } from 'react-router-dom' import { useSegmentAnalyticsContext } from '@/providers/segmentAnalytics/useSegmentAnalyticsContext' import { YppRequirementsErrorCode } from '@/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.types' @@ -19,6 +20,7 @@ type PageViewParams = { tab?: string utm_source?: string utm_campaign?: string + utm_content?: string isYppFlow?: boolean } & VideoPageViewParams & ChannelPageViewParams @@ -50,6 +52,7 @@ type YppOptInParams = { referrerId?: string utmSource?: string utmCampaign?: string + utmContent?: string } type IdentifyUserParams = { @@ -64,21 +67,31 @@ type playbackEventType = 'playbackStarted' | 'playbackPaused' | 'playbackResumed export const useSegmentAnalytics = () => { const { analytics } = useSegmentAnalyticsContext() + const [searchParams] = useSearchParams() const playbackEventsQueue = useRef<{ type: playbackEventType; params: videoPlaybackParams }[]>([]) + const getUTMParams = useCallback(() => { + const [referrer, utmSource, utmCampaign] = [ + searchParams.get('referrerId'), + searchParams.get('utm_source'), + searchParams.get('utm_campaign'), + ] + return { referrer, utmSource, utmCampaign } + }, [searchParams]) + const identifyUser = useCallback( (params: IdentifyUserParams) => { - analytics.identify(params.email, params) + analytics.identify(params.email, { ...params, ...getUTMParams() }) }, - [analytics] + [analytics, getUTMParams] ) const trackPageView = useCallback( (name: string, params?: PageViewParams) => { - analytics.page(undefined, name, params) + analytics.page(undefined, name, { ...params, ...getUTMParams() }) }, - [analytics] + [analytics, getUTMParams] ) const trackYppOptIn = useCallback( @@ -101,9 +114,10 @@ export const useSegmentAnalytics = () => { analytics.track('Membership created', { handle, email, + ...getUTMParams(), }) }, - [analytics] + [analytics, getUTMParams] ) const trackChannelCreation = useCallback( @@ -378,7 +392,7 @@ export const useSegmentAnalytics = () => { ) const trackTokenMintingCompleted = useCallback( - (channelId: string, tokenId: string, tokenTicker: string, initSupply: string, safetyOption: string) => { + (channelId: string, tokenId: string, tokenTicker: string, initSupply: number, safetyOption: string) => { analytics.track('Token minting completed', { channelId, tokenId, @@ -413,7 +427,7 @@ export const useSegmentAnalytics = () => { ) const trackAMMTokensPurchased = useCallback( - (tokenId: string, tokenTicker: string, channelId: string, crtAmount: string, joyPaid: string) => { + (tokenId: string, tokenTicker: string, channelId: string, crtAmount: number, joyPaid: number) => { analytics.track('Token Market Purchase', { tokenId, tokenTicker, @@ -426,7 +440,7 @@ export const useSegmentAnalytics = () => { ) const trackAMMTokensSold = useCallback( - (tokenId: string, tokenTicker: string, channelId: string, crtAmount: string, joyReceived: string) => { + (tokenId: string, tokenTicker: string, channelId: string, crtAmount: number, joyReceived: number) => { analytics.track('Token Market Sell', { tokenId, tokenTicker, @@ -438,6 +452,28 @@ export const useSegmentAnalytics = () => { [analytics] ) + const trackRevenueShareStarted = useCallback( + (channelId: string, tokenId: string, tokenTicker: string) => { + analytics.track('Revenue Share Started', { + channelId, + tokenId, + tokenTicker, + }) + }, + [analytics] + ) + + const trackRevenueShareClosed = useCallback( + (channelId: string, tokenId: string, tokenTicker: string) => { + analytics.track('Revenue Share Closed', { + channelId, + tokenId, + tokenTicker, + }) + }, + [analytics] + ) + const runNextQueueEvent = useCallback(async () => { const queueEvent = playbackEventsQueue.current.shift() if (!queueEvent) { @@ -501,6 +537,8 @@ export const useSegmentAnalytics = () => { trackPageView, trackPublishAndUploadClicked, trackReferralLinkGenerated, + trackRevenueShareClosed, + trackRevenueShareStarted, trackTokenMintingCompleted, trackTokenMintingStarted, trackUploadVideoClicked, diff --git a/packages/atlas/src/joystream-lib/extrinsics.ts b/packages/atlas/src/joystream-lib/extrinsics.ts index 6e582ccd52..729482f206 100644 --- a/packages/atlas/src/joystream-lib/extrinsics.ts +++ b/packages/atlas/src/joystream-lib/extrinsics.ts @@ -1241,7 +1241,7 @@ export class JoystreamLibExtrinsics { startAmmTx = async (memberId: MemberId, channelId: ChannelId, joySlopeNumber: number) => { const member = createType('PalletContentPermissionsContentActor', { Member: parseInt(memberId) }) return this.api.tx.content.activateAmm(member, parseInt(channelId), { - slope: createType('u128', new BN(HAPI_TO_JOY_RATE * joySlopeNumber)), + slope: createType('u128', BN.max(new BN(HAPI_TO_JOY_RATE * joySlopeNumber), new BN(1))), intercept: createType('u128', new BN(0)), }) } diff --git a/packages/atlas/src/providers/uploads/uploads.manager.tsx b/packages/atlas/src/providers/uploads/uploads.manager.tsx index 863ef1a900..790ce79cad 100644 --- a/packages/atlas/src/providers/uploads/uploads.manager.tsx +++ b/packages/atlas/src/providers/uploads/uploads.manager.tsx @@ -51,9 +51,6 @@ export const UploadsManager: FC = () => { const { getDataObjectsAvailability, dataObjects, startPolling, stopPolling } = useDataObjectsAvailabilityLazy({ fetchPolicy: 'network-only', - onCompleted: () => { - startPolling?.(atlasConfig.storage.assetUploadStatusPollingInterval) - }, }) // display snackbar when video upload is complete @@ -95,8 +92,10 @@ export const UploadsManager: FC = () => { if (!processingAssets.length) { return } - getDataObjectsAvailability(processingAssets.map((asset) => asset.id)) - }, [getDataObjectsAvailability, processingAssets]) + getDataObjectsAvailability(processingAssets.map((asset) => asset.id)).then(() => { + startPolling?.(atlasConfig.storage.assetUploadStatusPollingInterval) + }) + }, [getDataObjectsAvailability, processingAssets, startPolling]) useEffect(() => { dataObjects?.forEach((asset) => { diff --git a/packages/atlas/src/providers/ypp/ypp.store.ts b/packages/atlas/src/providers/ypp/ypp.store.ts index bdebab9f31..ea738bbf40 100644 --- a/packages/atlas/src/providers/ypp/ypp.store.ts +++ b/packages/atlas/src/providers/ypp/ypp.store.ts @@ -7,6 +7,7 @@ type YppStoreState = { selectedChannelId: string | null utmSource: string | null utmCampaign: string | null + utmContent: string | null yppModalOpenName: YppModalStep shouldContinueYppFlowAfterLogin: boolean shouldContinueYppFlowAfterCreatingChannel: boolean @@ -18,6 +19,7 @@ type YppStoreActions = { setSelectedChannelId: (selectedChannelId: string | null) => void setUtmSource: (utmSource: string | null) => void setUtmCampaign: (utmCampaign: string | null) => void + setUtmContent: (utmContent: string | null) => void setYppModalOpenName: (modal: YppModalStep) => void setShouldContinueYppFlowAfterLogin: (shouldContinueYppFlow: boolean) => void setShouldContinueYppFlowAfterCreatingChannel: (shouldContinueYppFlow: boolean) => void @@ -31,6 +33,7 @@ export const useYppStore = createStore( selectedChannelId: null, utmSource: null, utmCampaign: null, + utmContent: null, yppModalOpenName: null, shouldContinueYppFlowAfterLogin: false, shouldContinueYppFlowAfterCreatingChannel: false, @@ -57,6 +60,11 @@ export const useYppStore = createStore( state.utmCampaign = utmCampaign }) }, + setUtmContent: (utmContent) => { + set((state) => { + state.utmContent = utmContent + }) + }, setYppModalOpenName: (modal) => { set((state) => { state.yppModalOpenName = modal diff --git a/packages/atlas/src/utils/crts.ts b/packages/atlas/src/utils/crts.ts index a77b4ffa5c..a8834c067b 100644 --- a/packages/atlas/src/utils/crts.ts +++ b/packages/atlas/src/utils/crts.ts @@ -53,13 +53,14 @@ export const getRevenueShareStatusForMember = ({ } if (currentBlock > endingAt) { - if (isFinalized) { - return 'finalized' - } if (hasMemberStaked && !hasRecovered) { return 'unlock' } + if (isFinalized) { + return 'finalized' + } + return 'past' } diff --git a/packages/atlas/src/utils/misc.ts b/packages/atlas/src/utils/misc.ts index 2a115c0bb8..0720ffa5a4 100644 --- a/packages/atlas/src/utils/misc.ts +++ b/packages/atlas/src/utils/misc.ts @@ -19,7 +19,7 @@ export const withTimeout = async (promise: Promise | Promise[], time } export const pluralizeNoun = (count: number, noun: string, formatCount?: boolean, suffix = 's') => - `${formatCount ? formatNumber(count) : count} ${noun}${count > 1 ? suffix : ''}` + `${formatCount ? formatNumber(count) : count} ${noun}${count !== 1 ? suffix : ''}` export const wait = (milliseconds: number): Promise => new Promise((resolve) => { diff --git a/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.tsx b/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.tsx index 26fdb27791..7e14f98a87 100644 --- a/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.tsx +++ b/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.tsx @@ -100,7 +100,8 @@ export const YppAuthorizationModal: FC = ({ unSynced ytResponseData, utmSource, utmCampaign, - actions: { setYtResponseData, setUtmSource, setUtmCampaign }, + utmContent, + actions: { setYtResponseData, setUtmSource, setUtmCampaign, setUtmContent }, } = useYppStore((store) => store, shallow) const setReferrerId = useYppStore((store) => store.actions.setReferrerId) const setShouldContinueYppFlowAfterLogin = useYppStore((store) => store.actions.setShouldContinueYppFlowAfterLogin) @@ -165,7 +166,10 @@ export const YppAuthorizationModal: FC = ({ unSynced if (searchParams.get('utm_campaign')) { setUtmCampaign(searchParams.get('utm_campaign')) } - }, [searchParams, setUtmCampaign, setUtmSource]) + if (searchParams.get('utm_content')) { + setUtmContent(searchParams.get('utm_content')) + } + }, [searchParams, setUtmCampaign, setUtmContent, setUtmSource]) useEffect(() => { contentRef.current?.scrollTo({ top: 0 }) @@ -312,6 +316,7 @@ export const YppAuthorizationModal: FC = ({ unSynced referrerId: data.referrerChannelId, utmSource: utmSource || undefined, utmCampaign: utmCampaign || undefined, + utmContent: utmContent || undefined, }) setReferrerId(null) setYtResponseData(null) diff --git a/packages/atlas/src/views/studio/CrtDashboard/CrtDashboard.tsx b/packages/atlas/src/views/studio/CrtDashboard/CrtDashboard.tsx index e8b6a9c696..dc1b163010 100644 --- a/packages/atlas/src/views/studio/CrtDashboard/CrtDashboard.tsx +++ b/packages/atlas/src/views/studio/CrtDashboard/CrtDashboard.tsx @@ -119,7 +119,7 @@ export const CrtDashboard = () => { ) : ( diff --git a/packages/atlas/src/views/studio/CrtDashboard/tabs/CrtDashboardMainTab.tsx b/packages/atlas/src/views/studio/CrtDashboard/tabs/CrtDashboardMainTab.tsx index 23e17b437f..e701e9d3cb 100644 --- a/packages/atlas/src/views/studio/CrtDashboard/tabs/CrtDashboardMainTab.tsx +++ b/packages/atlas/src/views/studio/CrtDashboard/tabs/CrtDashboardMainTab.tsx @@ -96,9 +96,9 @@ export const CrtDashboardMainTab = ({ token, onTabChange, hasOpenedMarket }: Crt } /> { const { activeChannel } = useUser() - const [openClaimShareModal, setOpenClaimShareModal] = useState(false) const memoizedChannelStateBloatBond = useMemo(() => { return new BN(activeChannel?.channelStateBloatBond || 0) }, [activeChannel?.channelStateBloatBond]) @@ -78,16 +76,7 @@ export const CrtRevenueTab = ({ token }: CrtRevenueTabProps) => { {activeRevenueShare ? ( <> - setOpenClaimShareModal(true)} - /> - setOpenClaimShareModal(false)} - show={openClaimShareModal} - tokenId={token.id} - /> + {activeRevenueShare.stakers.length ? ( diff --git a/packages/atlas/src/views/studio/MyPaymentsView/PaymentsOverview/PaymentsOverView.tsx b/packages/atlas/src/views/studio/MyPaymentsView/PaymentsOverview/PaymentsOverView.tsx index 32e068fc87..26a8899543 100644 --- a/packages/atlas/src/views/studio/MyPaymentsView/PaymentsOverview/PaymentsOverView.tsx +++ b/packages/atlas/src/views/studio/MyPaymentsView/PaymentsOverview/PaymentsOverView.tsx @@ -2,27 +2,42 @@ import { BN } from 'bn.js' import { useMemo, useState } from 'react' import { useFullChannel } from '@/api/hooks/channel' +import { useGetFullCreatorTokenQuery } from '@/api/queries/__generated__/creatorTokens.generated' import { SvgAlertsInformative24 } from '@/assets/icons' import { NumberFormat } from '@/components/NumberFormat' import { Text } from '@/components/Text' import { WidgetTile } from '@/components/WidgetTile' +import { StartRevenueShare } from '@/components/_crt/StartRevenueShareModal' import { ClaimChannelPaymentsDialog } from '@/components/_overlays/ClaimChannelPaymentsDialog' import { SendFundsDialog } from '@/components/_overlays/SendTransferDialogs' import { useMediaMatch } from '@/hooks/useMediaMatch' import { useSubscribeAccountBalance } from '@/providers/joystream' +import { useSnackbar } from '@/providers/snackbars' import { useUser } from '@/providers/user/user.hooks' import { useChannelPayout } from './PaymentsOverview.hooks' import { CustomNodeWrapper, StyledSvgJoyTokenMonochrome24, TilesWrapper } from './PaymentsOverview.styles' export const PaymentsOverView = () => { + const [openRevenueShareModal, setOpenRevenueShareModal] = useState(false) const { channelId, activeMembership } = useUser() + const { displaySnackbar } = useSnackbar() const [showWithdrawDialog, setShowWithdrawDialog] = useState(false) const [showClaimDialog, setShowClaimDialog] = useState(false) const { channel, loading } = useFullChannel(channelId || '') + const { data: tokenData } = useGetFullCreatorTokenQuery({ + variables: { + id: channel?.creatorToken?.token.id ?? '', + }, + skip: !channel?.creatorToken?.token.id, + }) const { availableAward, isAwardLoading } = useChannelPayout() const { totalBalance } = useSubscribeAccountBalance() + const hasOpenedRevenueShare = tokenData?.creatorTokenById?.revenueShares.some( + (revenueShare) => !revenueShare.finalized + ) + const memoizedChannelStateBloatBond = useMemo(() => { return new BN(channel?.channelStateBloatBond || 0) }, [channel?.channelStateBloatBond]) @@ -88,14 +103,38 @@ export const PaymentsOverView = () => { /> } loading={loading || channelBalance === undefined} - button={{ - text: 'Withdraw', - variant: 'secondary', - fullWidth: !mdMatch, - onClick: () => setShowWithdrawDialog(true), - }} + button={ + channel?.creatorToken?.token.id + ? { + text: 'Revenue share', + variant: 'secondary', + fullWidth: !mdMatch, + onClick: () => + hasOpenedRevenueShare + ? displaySnackbar({ + title: hasOpenedRevenueShare + ? 'You already have active revenue share' + : 'You can not start a revenue share while the market is open', + iconType: 'info', + }) + : setOpenRevenueShareModal(true), + } + : { + text: 'Withdraw', + variant: 'secondary', + fullWidth: !mdMatch, + onClick: () => setShowWithdrawDialog(true), + } + } /> + {tokenData?.creatorTokenById ? ( + setOpenRevenueShareModal(false)} + /> + ) : null} ) } diff --git a/packages/atlas/src/views/studio/MyPaymentsView/PaymentsTransactions/PaymentTransactions.hooks.ts b/packages/atlas/src/views/studio/MyPaymentsView/PaymentsTransactions/PaymentTransactions.hooks.ts index 35564e0c9c..4d3c260708 100644 --- a/packages/atlas/src/views/studio/MyPaymentsView/PaymentsTransactions/PaymentTransactions.hooks.ts +++ b/packages/atlas/src/views/studio/MyPaymentsView/PaymentsTransactions/PaymentTransactions.hooks.ts @@ -1,5 +1,6 @@ import { useGetChannelPaymentEventsQuery } from '@/api/queries/__generated__/channels.generated' import { useJoystream } from '@/providers/joystream' +import { useUser } from '@/providers/user/user.hooks' import { mapEventToPaymentHistory } from './PaymentTransactions.utils' @@ -7,6 +8,7 @@ export const useChannelPaymentsHistory = (channelId: string) => { const { chainState: { nftPlatformFeePercentage }, } = useJoystream() + const { memberId } = useUser() const { data, refetch, ...rest } = useGetChannelPaymentEventsQuery({ variables: { channelId: channelId ?? '-1', @@ -17,7 +19,7 @@ export const useChannelPaymentsHistory = (channelId: string) => { return { ...rest, rawData: data, - paymentData: data?.events.map(mapEventToPaymentHistory(nftPlatformFeePercentage)), + paymentData: data?.events.map(mapEventToPaymentHistory(nftPlatformFeePercentage, memberId ?? '')), loading: rest.loading, fetchPaymentsData: refetch, } diff --git a/packages/atlas/src/views/studio/MyPaymentsView/PaymentsTransactions/PaymentTransactions.utils.ts b/packages/atlas/src/views/studio/MyPaymentsView/PaymentsTransactions/PaymentTransactions.utils.ts index ff7b2112d7..c2ee509a2c 100644 --- a/packages/atlas/src/views/studio/MyPaymentsView/PaymentsTransactions/PaymentTransactions.utils.ts +++ b/packages/atlas/src/views/studio/MyPaymentsView/PaymentsTransactions/PaymentTransactions.utils.ts @@ -2,6 +2,7 @@ import BN from 'bn.js' import { GetChannelPaymentEventsQuery } from '@/api/queries/__generated__/channels.generated' import { PaymentHistory } from '@/components/TablePaymentsHistory' +import { permillToPercentage } from '@/utils/number' type EventData = GetChannelPaymentEventsQuery['events'][number]['data'] & { nftPlatformFeePercentage: number @@ -23,12 +24,14 @@ const getType = (eventData: EventData): PaymentHistory['type'] => { return 'withdrawal' case 'ChannelPaymentMadeEventData': return 'direct-payment' + case 'CreatorTokenRevenueSplitIssuedEventData': + return 'revenue-share' default: throw Error('Unknown event') } } -const getAmount = (eventData: EventData): BN => { +const getAmount = (eventData: EventData, memberId: string): BN => { switch (eventData.__typename) { case 'NftBoughtEventData': { if (eventData.previousNftOwner.__typename !== 'NftOwnerChannel') { @@ -51,6 +54,13 @@ const getAmount = (eventData: EventData): BN => { case 'ChannelRewardClaimedEventData': case 'ChannelPaymentMadeEventData': return new BN(eventData.amount) + case 'CreatorTokenRevenueSplitIssuedEventData': { + const channelAsStaker = eventData.revenueShare?.stakers.find((staker) => staker.account.member.id === memberId) + return new BN(eventData.revenueShare?.allocation ?? 0) + .muln(100 - permillToPercentage(eventData.token?.revenueShareRatioPermill ?? 0)) + .divn(100) + .add(new BN(channelAsStaker?.earnings ?? 0)) + } default: throw Error('Unknown event') } @@ -70,6 +80,8 @@ const getSender = (eventData: EventData) => { return eventData.actor.__typename === 'ContentActorMember' ? eventData.actor.member.controllerAccount : 'council' case 'ChannelPaymentMadeEventData': return eventData.payer.controllerAccount + case 'CreatorTokenRevenueSplitIssuedEventData': + return 'own-channel' default: throw Error('Unknown event') } @@ -96,20 +108,22 @@ const getDescription = (eventData: EventData) => { return '' case 'ChannelPaymentMadeEventData': return eventData.rationale + case 'CreatorTokenRevenueSplitIssuedEventData': + return '' default: return undefined } } export const mapEventToPaymentHistory = - (nftPlatformFeePercentage: number) => + (nftPlatformFeePercentage: number, memberId: string) => (event: GetChannelPaymentEventsQuery['events'][number]): PaymentHistory => { const { inBlock, timestamp } = event const eventData = { ...event.data, nftPlatformFeePercentage } return { type: getType(eventData), block: inBlock, - amount: getAmount(eventData), + amount: getAmount(eventData, memberId), date: new Date(timestamp), description: getDescription(eventData) || '-', sender: getSender(eventData), @@ -123,6 +137,11 @@ export const aggregatePaymentHistory = (arg: PaymentHistory[]) => prev.totalWithdrawn.iadd(next.amount.abs()) return prev } + // revenue share is both earned and withdrawn at the time + if (next.type === 'revenue-share') { + prev.totalWithdrawn.iadd(next.amount.abs()) + } + prev.totalEarned.iadd(next.amount) return prev }, diff --git a/packages/atlas/src/views/studio/StudioLayout.tsx b/packages/atlas/src/views/studio/StudioLayout.tsx index 65137ce77e..eaf5d44960 100644 --- a/packages/atlas/src/views/studio/StudioLayout.tsx +++ b/packages/atlas/src/views/studio/StudioLayout.tsx @@ -72,6 +72,8 @@ const locationToPageName = { '/crt-dashboard': 'CRT Dashboard', '/crt-preview-edit': 'CRT Preview Edit', '/crt-preview': 'CRT Preview', + '/crt/edit': 'CRT Edit', + '/crt': 'CRT Landing', 'video-workspace': 'Video workspace', '/uploads': 'Uploads', '/signin': 'Sign in', diff --git a/packages/atlas/src/views/viewer/ChannelView/ChannelViewTabs/ChannelToken.tsx b/packages/atlas/src/views/viewer/ChannelView/ChannelViewTabs/ChannelToken.tsx index 465ce7555c..357866cc98 100644 --- a/packages/atlas/src/views/viewer/ChannelView/ChannelViewTabs/ChannelToken.tsx +++ b/packages/atlas/src/views/viewer/ChannelView/ChannelViewTabs/ChannelToken.tsx @@ -31,6 +31,7 @@ export const ChannelToken = ({ tokenId, memberId, cumulativeRevenue }: ChannelTo variables: { id: tokenId ?? '', }, + fetchPolicy: 'cache-and-network', }) const { activeMembership, setActiveChannel } = useUser() const isChannelOwner = activeMembership?.channels.some((channel) => channel.id === id) @@ -111,7 +112,7 @@ export const ChannelToken = ({ tokenId, memberId, cumulativeRevenue }: ChannelTo ) @@ -120,6 +121,7 @@ export const ChannelToken = ({ tokenId, memberId, cumulativeRevenue }: ChannelTo diff --git a/packages/atlas/src/views/viewer/PortfolioView/PortfolioView.tsx b/packages/atlas/src/views/viewer/PortfolioView/PortfolioView.tsx index 54e1510c24..2ffa79df80 100644 --- a/packages/atlas/src/views/viewer/PortfolioView/PortfolioView.tsx +++ b/packages/atlas/src/views/viewer/PortfolioView/PortfolioView.tsx @@ -1,10 +1,11 @@ import styled from '@emotion/styled' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { SvgActionCreatorToken, SvgActionPlay } from '@/assets/icons' import { LimitedWidthContainer } from '@/components/LimitedWidthContainer' import { PageTabs } from '@/components/PageTabs' import { useMediaMatch } from '@/hooks/useMediaMatch' +import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics' import { sizes } from '@/styles' import { PortfolioNftTab } from '@/views/viewer/PortfolioView/tabs/PortfolioNftTab' import { PortfolioTokenTab } from '@/views/viewer/PortfolioView/tabs/PortfolioTokenTab' @@ -25,6 +26,11 @@ const TABS = [ export const PortfolioView = () => { const [tab, setTab] = useState(0) const smMatch = useMediaMatch('sm') + const { trackPageView } = useSegmentAnalytics() + + useEffect(() => { + trackPageView('Portfolio', { tab: TABS[tab].name }) + }, [tab, trackPageView]) return ( <> diff --git a/packages/atlas/src/views/viewer/PortfolioView/tabs/PortfolioTokenTab.tsx b/packages/atlas/src/views/viewer/PortfolioView/tabs/PortfolioTokenTab.tsx index 891bc7fef7..568ce4b813 100644 --- a/packages/atlas/src/views/viewer/PortfolioView/tabs/PortfolioTokenTab.tsx +++ b/packages/atlas/src/views/viewer/PortfolioView/tabs/PortfolioTokenTab.tsx @@ -79,7 +79,6 @@ export const PortfolioTokenTab = () => { skip: !memberId, }) const commonParams = { - finalized_eq: false, token: { id_in: data?.tokenAccounts.map(({ token }) => token.id), }, @@ -88,11 +87,13 @@ export const PortfolioTokenTab = () => { OR: [ { ...commonParams, + finalized_eq: false, endsAt_gt: timestamp, }, { ...commonParams, stakers_some: { + recovered_eq: false, account: { member: { id_eq: memberId, @@ -128,7 +129,7 @@ export const PortfolioTokenTab = () => { isVerified: false, tokenId: tokenAccount.token.id, memberId: memberId ?? '', - vested: tokenAccount.vestingSchedules.reduce((prev, next) => prev + Number(next.totalVestingAmount), 0), + staked: +(tokenAccount.stakedAmount ?? 0), total: +tokenAccount.totalAmount, channelId: tokenAccount.token.channel?.channel.id ?? '', hasStaked: +tokenAccount.stakedAmount > 0, diff --git a/packages/atlas/src/views/viewer/ViewerLayout.tsx b/packages/atlas/src/views/viewer/ViewerLayout.tsx index 4d799e19b6..7a5f081a63 100644 --- a/packages/atlas/src/views/viewer/ViewerLayout.tsx +++ b/packages/atlas/src/views/viewer/ViewerLayout.tsx @@ -79,6 +79,7 @@ const locationToPageName = { '/member/': 'Member', '/notifications': 'Notifications', '/marketplace': 'Marketplace', + '/portfolio': 'Portfolio', '/ypp': 'YPP landing page', '/ypp-dashboard': 'YPP Dashboard', '/referrals': 'Referrals Landing page', @@ -217,11 +218,12 @@ const MiscUtils = () => { if (['Channel', 'Category', 'Video'].some((page) => pageName?.includes(page))) { return } - const [query, referrerChannel, utmSource, utmCampaign, gState, gCode] = [ + const [query, referrerChannel, utmSource, utmCampaign, utmContent, gState, gCode] = [ searchParams.get('query'), searchParams.get('referrerId'), searchParams.get('utm_source'), searchParams.get('utm_campaign'), + searchParams.get('utm_content'), searchParams.get('state'), searchParams.get('code'), ] @@ -233,13 +235,10 @@ const MiscUtils = () => { const trackRequestTimeout = setTimeout( () => trackPageView(pageName || 'Unknown page', { - ...(location.pathname === absoluteRoutes.viewer.ypp() - ? { - referrerChannel: referrerChannel || undefined, - utm_source: utmSource || undefined, - utm_campaign: utmCampaign || undefined, - } - : {}), + referrerChannel: referrerChannel || undefined, + utm_source: utmSource || undefined, + utm_campaign: utmCampaign || undefined, + utm_content: utmContent || undefined, ...(location.pathname === absoluteRoutes.viewer.search() ? { searchQuery: query } : {}), }), 1000