From dc9b7cdc7b4e0c05234d88f7e69954255b9c070a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Apr 2026 21:59:26 +0000 Subject: [PATCH 1/4] Add unlock modal for locked text posts The Unlock button and Members Only text on locked text posts now open a "How to Unlock" modal showing the coin requirement and a Buy Coins button that opens the buy/sell modal. https://claude.ai/code/session_01Nn1WcA8yq5kJA2Xrm2ZBWv --- .../components/LockedTextPostModal.tsx | 77 +++++++++++++++++++ .../components/TextPostCard.tsx | 18 ++++- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 packages/web/src/pages/fan-club-detail-page/components/LockedTextPostModal.tsx diff --git a/packages/web/src/pages/fan-club-detail-page/components/LockedTextPostModal.tsx b/packages/web/src/pages/fan-club-detail-page/components/LockedTextPostModal.tsx new file mode 100644 index 00000000000..af431f21de7 --- /dev/null +++ b/packages/web/src/pages/fan-club-detail-page/components/LockedTextPostModal.tsx @@ -0,0 +1,77 @@ +import { useCallback } from 'react' + +import { useArtistCoin } from '@audius/common/api' +import { useBuySellModal } from '@audius/common/store' +import { + Button, + Flex, + IconArtistCoin, + IconLock, + Modal, + ModalContent, + ModalHeader, + ModalTitle, + Text +} from '@audius/harmony' + +const messages = { + howToUnlock: 'HOW TO UNLOCK', + description: 'To unlock this post, you need to hold', + buyCoins: 'Buy Coins' +} + +type LockedTextPostModalProps = { + isOpen: boolean + onClose: () => void + mint: string +} + +export const LockedTextPostModal = ({ + isOpen, + onClose, + mint +}: LockedTextPostModalProps) => { + const { data: coin } = useArtistCoin(mint) + const { onOpen: openBuySellModal } = useBuySellModal() + + const handleBuyCoins = useCallback(() => { + if (!coin?.ticker) return + openBuySellModal({ isOpen: true, ticker: coin.ticker }) + onClose() + }, [coin?.ticker, openBuySellModal, onClose]) + + return ( + + + } + /> + + + + + {messages.description}{' '} + {coin?.ticker ? ( + + ${coin.ticker} + + ) : ( + "the artist's coins" + )} + . + + + + + + ) +} diff --git a/packages/web/src/pages/fan-club-detail-page/components/TextPostCard.tsx b/packages/web/src/pages/fan-club-detail-page/components/TextPostCard.tsx index df545db7db5..586fdaafc1f 100644 --- a/packages/web/src/pages/fan-club-detail-page/components/TextPostCard.tsx +++ b/packages/web/src/pages/fan-club-detail-page/components/TextPostCard.tsx @@ -28,6 +28,8 @@ import { ComposerInput } from 'components/composer-input/ComposerInput' import { ConfirmationModal } from 'components/confirmation-modal' import { UserLink } from 'components/link' +import { LockedTextPostModal } from './LockedTextPostModal' + const messages = { unlock: 'Unlock', membersOnly: 'Members Only', @@ -57,6 +59,7 @@ export const TextPostCard = ({ commentId, mint }: TextPostCardProps) => { const [isEditing, setIsEditing] = useState(false) const [editMessageId, setEditMessageId] = useState(0) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) + const [showUnlockModal, setShowUnlockModal] = useState(false) const isOwner = currentUserId != null && comment?.userId === currentUserId const isCoinOwner = currentUserId != null && coin?.ownerId === currentUserId @@ -179,10 +182,17 @@ export const TextPostCard = ({ commentId, mint }: TextPostCardProps) => { size='small' rounded css={{ height: '24px' }} + onClick={() => setShowUnlockModal(true)} > {messages.unlock} - + setShowUnlockModal(true)} + css={{ cursor: 'pointer' }} + > {messages.membersOnly} @@ -267,6 +277,12 @@ export const TextPostCard = ({ commentId, mint }: TextPostCardProps) => { onConfirm={handleDelete} onClose={() => setShowDeleteConfirm(false)} /> + + setShowUnlockModal(false)} + mint={mint} + /> ) } From 2fe256f264e886821f8531161bd928536c505326 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Thu, 2 Apr 2026 16:30:47 -0700 Subject: [PATCH 2/4] Fix lint --- .../fan-club-detail-page/components/LockedTextPostModal.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/web/src/pages/fan-club-detail-page/components/LockedTextPostModal.tsx b/packages/web/src/pages/fan-club-detail-page/components/LockedTextPostModal.tsx index af431f21de7..2560a00d532 100644 --- a/packages/web/src/pages/fan-club-detail-page/components/LockedTextPostModal.tsx +++ b/packages/web/src/pages/fan-club-detail-page/components/LockedTextPostModal.tsx @@ -43,10 +43,7 @@ export const LockedTextPostModal = ({ return ( - } - /> + } /> From 39e5f19510d315168057048580851af744c7cd1a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Apr 2026 23:34:33 +0000 Subject: [PATCH 3/4] Add unlock flow for locked text posts on mobile Tapping the locked text post area now reveals a "How To Unlock" section showing the coin requirement and a "Buy Coins" button that navigates to the BuySell screen, matching the pattern used for gated tracks. https://claude.ai/code/session_01Nn1WcA8yq5kJA2Xrm2ZBWv --- .../components/FanClubTab.tsx | 6 +- .../components/TextPostCard.tsx | 96 +++++++++++++++---- 2 files changed, 84 insertions(+), 18 deletions(-) diff --git a/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx b/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx index b0cf16e0359..c6701d29d28 100644 --- a/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx +++ b/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx @@ -305,7 +305,11 @@ const FanClubFeed = ({ {hasTextPosts ? textPosts.map((item) => ( - + )) : null} diff --git a/packages/mobile/src/screens/coin-details-screen/components/TextPostCard.tsx b/packages/mobile/src/screens/coin-details-screen/components/TextPostCard.tsx index 76c9ee6acd0..90ea0e16b8a 100644 --- a/packages/mobile/src/screens/coin-details-screen/components/TextPostCard.tsx +++ b/packages/mobile/src/screens/coin-details-screen/components/TextPostCard.tsx @@ -1,8 +1,12 @@ -import { useComment } from '@audius/common/api' +import { useCallback, useState } from 'react' + +import { useArtistCoin, useComment } from '@audius/common/api' import type { ID } from '@audius/common/models' import { getLargestTimeUnitText } from '@audius/common/utils' +import { TouchableOpacity } from 'react-native' import { + Button, Flex, IconLock, Paper, @@ -12,18 +16,36 @@ import { } from '@audius/harmony-native' import { ProfilePicture } from 'app/components/core' import { UserLink } from 'app/components/user-link' +import { useNavigation } from 'app/hooks/useNavigation' const messages = { - locked: 'Hold this coin to unlock' + locked: 'Hold this coin to unlock', + howToUnlock: 'How To Unlock', + description: 'To unlock this post, you need to hold', + buyCoins: 'Buy Coins' } type TextPostCardProps = { commentId: ID + mint: string } -export const TextPostCard = ({ commentId }: TextPostCardProps) => { +export const TextPostCard = ({ commentId, mint }: TextPostCardProps) => { const { data: comment, isPending } = useComment(commentId) + const { data: coin } = useArtistCoin(mint) const { color, spacing, cornerRadius } = useTheme() + const navigation = useNavigation() + const [showUnlock, setShowUnlock] = useState(false) + + const handleBuyCoins = useCallback(() => { + if (coin?.ticker) { + navigation.navigate('BuySell', { + initialTab: 'buy', + coinTicker: coin.ticker + }) + setShowUnlock(false) + } + }, [coin?.ticker, navigation]) if (isPending) { return ( @@ -76,20 +98,60 @@ export const TextPostCard = ({ commentId }: TextPostCardProps) => { {isLocked ? ( - - - - {messages.locked} - + + setShowUnlock(true)} + activeOpacity={0.7} + > + + + + {messages.locked} + + + + {showUnlock ? ( + + + + + {messages.howToUnlock} + + + + {messages.description}{' '} + + {coin?.ticker ? `$${coin.ticker}` : "the artist's coins"} + + . + + + + ) : null} ) : ( From 8a676585bc2078644499c1e26a81b8ba0b7d17ae Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Thu, 2 Apr 2026 17:13:35 -0700 Subject: [PATCH 4/4] Fix lint --- .../lineup-tile/LineupTileAccessStatus.tsx | 12 +--- .../LockedTextPostDrawer.tsx | 7 +- .../components/FanClubTab.tsx | 4 +- .../components/TextPostCard.tsx | 67 +++++-------------- 4 files changed, 18 insertions(+), 72 deletions(-) diff --git a/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx b/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx index b49ae7a1ad4..f6e2ea32ae9 100644 --- a/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx +++ b/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx @@ -1,11 +1,6 @@ import { useCallback } from 'react' -import { useArtistCoin } from '@audius/common/api' -import type { - ID, - AccessConditions, - TokenGatedConditions -} from '@audius/common/models' +import type { ID, AccessConditions } from '@audius/common/models' import { ModalSource, isContentTokenGated, @@ -84,11 +79,6 @@ export const LineupTileAccessStatus = ({ const isTokenGated = isContentTokenGated(streamConditions) const isUnlocking = gatedTrackStatus === 'UNLOCKING' - const { data: token } = useArtistCoin( - (streamConditions as TokenGatedConditions)?.token_gate?.token_mint, - { enabled: isTokenGated } - ) - const handlePress = useCallback(() => { if (hasStreamAccess) { return diff --git a/packages/mobile/src/components/locked-text-post-drawer/LockedTextPostDrawer.tsx b/packages/mobile/src/components/locked-text-post-drawer/LockedTextPostDrawer.tsx index 3520215cfef..2cb8a706c0b 100644 --- a/packages/mobile/src/components/locked-text-post-drawer/LockedTextPostDrawer.tsx +++ b/packages/mobile/src/components/locked-text-post-drawer/LockedTextPostDrawer.tsx @@ -65,12 +65,7 @@ export const LockedTextPostDrawer = () => { width={spacing(6)} height={spacing(6)} /> - + {messages.howToUnlock} diff --git a/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx b/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx index 3173e5281da..cd7ed8681a5 100644 --- a/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx +++ b/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx @@ -381,9 +381,7 @@ export const FanClubTab = ({ mint, onSwitchToCoinTab }: FanClubTabProps) => { ) : null} - {membershipKnown ? ( - - ) : null} + {membershipKnown ? : null} ) } diff --git a/packages/mobile/src/screens/coin-details-screen/components/TextPostCard.tsx b/packages/mobile/src/screens/coin-details-screen/components/TextPostCard.tsx index 6aeaee25455..dbcfe5b5a3b 100644 --- a/packages/mobile/src/screens/coin-details-screen/components/TextPostCard.tsx +++ b/packages/mobile/src/screens/coin-details-screen/components/TextPostCard.tsx @@ -3,8 +3,6 @@ import { useCallback } from 'react' import { useComment } from '@audius/common/api' import type { ID } from '@audius/common/models' import { getLargestTimeUnitText } from '@audius/common/utils' -import { View } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' import { Button, @@ -12,8 +10,7 @@ import { IconLock, Paper, Skeleton, - Text, - useTheme + Text } from '@audius/harmony-native' import { ProfilePicture } from 'app/components/core' import { UserLink } from 'app/components/user-link' @@ -24,26 +21,6 @@ const messages = { membersOnly: 'Members Only' } -/** - * Generate a deterministic pseudo-random placeholder string for locked posts - * so each post looks visually distinct behind the blur. - */ -const generatePlaceholder = (commentId: ID) => { - const seed = Number(commentId) - const wordCount = 5 + (seed % 21) // 5–25 words - const words: string[] = [] - const pool = 'abcdefghijklmnopqrstuvwxyz' - for (let i = 0; i < wordCount; i++) { - const len = 3 + ((seed * (i + 1) * 7) % 8) // 3–10 chars per word - let word = '' - for (let j = 0; j < len; j++) { - word += pool[(seed * (i + 1) * (j + 1) * 13) % pool.length] - } - words.push(word) - } - return words.join(' ') -} - type TextPostCardProps = { commentId: ID mint: string @@ -51,7 +28,6 @@ type TextPostCardProps = { export const TextPostCard = ({ commentId, mint }: TextPostCardProps) => { const { data: comment, isPending } = useComment(commentId) - const { color } = useTheme() const { onOpen: openLockedTextPostDrawer } = useDrawer('LockedTextPost') const handleUnlock = useCallback(() => { @@ -109,34 +85,21 @@ export const TextPostCard = ({ commentId, mint }: TextPostCardProps) => { {isLocked ? ( - - - - {generatePlaceholder(commentId)} + + + + + + {messages.membersOnly} - - - - - - - - {messages.membersOnly} - - ) : (