diff --git a/packages/mobile/src/app/Drawers.tsx b/packages/mobile/src/app/Drawers.tsx
index 2c9c9e44e98..b36a6f738ae 100644
--- a/packages/mobile/src/app/Drawers.tsx
+++ b/packages/mobile/src/app/Drawers.tsx
@@ -30,6 +30,7 @@ import { HostRemixContestDrawer } from 'app/components/host-remix-contest-drawer
import { InboxUnavailableDrawer } from 'app/components/inbox-unavailable-drawer/InboxUnavailableDrawer'
import { LeavingAudiusDrawer } from 'app/components/leaving-audius-drawer'
import { LockedContentDrawer } from 'app/components/locked-content-drawer'
+import { LockedTextPostDrawer } from 'app/components/locked-text-post-drawer'
import { ManagerModeDrawer } from 'app/components/manager-mode-drawer/ManagerModeDrawer'
import { OverflowMenuDrawer } from 'app/components/overflow-menu-drawer'
import { PlaybackRateDrawer } from 'app/components/playback-rate-drawer'
@@ -163,6 +164,7 @@ const nativeDrawersMap: { [DrawerName in Drawer]?: ComponentType } = {
RemoveDownloadedFavorites: RemoveDownloadedFavoritesDrawer,
UnfavoriteDownloadedCollection: UnfavoriteDownloadedCollectionDrawer,
LockedContent: LockedContentDrawer,
+ LockedTextPost: LockedTextPostDrawer,
ChatActions: ChatActionsDrawer,
CreateChatActions: CreateChatActionsDrawer,
BlockMessages: BlockMessagesDrawer,
diff --git a/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx b/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx
index f46ddf560b8..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,
@@ -33,7 +28,6 @@ import {
} from '@audius/harmony-native'
import LoadingSpinner from 'app/components/loading-spinner'
import { useIsUSDCEnabled } from 'app/hooks/useIsUSDCEnabled'
-import { useNavigation } from 'app/hooks/useNavigation'
import { make, track } from 'app/services/analytics'
import { setVisibility } from 'app/store/drawers/slice'
import { makeStyles } from 'app/styles'
@@ -84,12 +78,6 @@ export const LineupTileAccessStatus = ({
isUSDCEnabled && isContentUSDCPurchaseGated(streamConditions)
const isTokenGated = isContentTokenGated(streamConditions)
const isUnlocking = gatedTrackStatus === 'UNLOCKING'
- const navigation = useNavigation()
-
- const { data: token } = useArtistCoin(
- (streamConditions as TokenGatedConditions)?.token_gate?.token_mint,
- { enabled: isTokenGated }
- )
const handlePress = useCallback(() => {
if (hasStreamAccess) {
@@ -120,13 +108,6 @@ export const LineupTileAccessStatus = ({
source: determineModalSource()
}
)
- } else if (isTokenGated) {
- if (token?.ticker) {
- navigation.push('BuySell', {
- initialTab: 'buy',
- coinTicker: token.ticker
- })
- }
} else if (contentId) {
dispatch(setLockedContentId({ id: contentId }))
dispatch(setVisibility({ drawer: 'LockedContent', visible: true }))
@@ -134,13 +115,10 @@ export const LineupTileAccessStatus = ({
}, [
hasStreamAccess,
isUSDCPurchase,
- isTokenGated,
contentId,
contentType,
openPremiumContentPurchaseModal,
tileSource,
- token?.ticker,
- navigation,
dispatch
])
diff --git a/packages/mobile/src/components/locked-text-post-drawer/LockedTextPostDrawer.tsx b/packages/mobile/src/components/locked-text-post-drawer/LockedTextPostDrawer.tsx
new file mode 100644
index 00000000000..2cb8a706c0b
--- /dev/null
+++ b/packages/mobile/src/components/locked-text-post-drawer/LockedTextPostDrawer.tsx
@@ -0,0 +1,92 @@
+import { useCallback } from 'react'
+
+import { useArtistCoin } from '@audius/common/api'
+import { View } from 'react-native'
+
+import { Button, Flex, IconLock, Text } from '@audius/harmony-native'
+import { NativeDrawer } from 'app/components/drawer'
+import { useDrawer } from 'app/hooks/useDrawer'
+import { useNavigation } from 'app/hooks/useNavigation'
+import { makeStyles, flexRowCentered } from 'app/styles'
+import { spacing } from 'app/styles/spacing'
+import { useColor } from 'app/utils/theme'
+
+const DRAWER_NAME = 'LockedTextPost'
+
+const messages = {
+ howToUnlock: 'HOW TO UNLOCK',
+ description: 'To unlock this post, you need to hold',
+ buyCoins: 'Buy Coins'
+}
+
+const useStyles = makeStyles(({ spacing, palette }) => ({
+ drawer: {
+ paddingVertical: spacing(6),
+ alignItems: 'center',
+ backgroundColor: palette.white,
+ paddingHorizontal: spacing(4),
+ gap: spacing(6)
+ },
+ titleContainer: {
+ ...flexRowCentered(),
+ justifyContent: 'center',
+ paddingBottom: spacing(4),
+ gap: spacing(2),
+ borderBottomColor: palette.neutralLight8,
+ borderBottomWidth: 1,
+ width: '100%'
+ }
+}))
+
+export const LockedTextPostDrawer = () => {
+ const styles = useStyles()
+ const neutralLight2 = useColor('neutralLight2')
+ const navigation = useNavigation()
+ const { data, onClose } = useDrawer('LockedTextPost')
+ const mint = (data as { mint: string } | undefined)?.mint
+ const { data: coin } = useArtistCoin(mint)
+
+ const handleBuyCoins = useCallback(() => {
+ if (coin?.ticker) {
+ onClose()
+ navigation.navigate('BuySell', {
+ initialTab: 'buy',
+ coinTicker: coin.ticker
+ })
+ }
+ }, [coin?.ticker, navigation, onClose])
+
+ return (
+
+
+
+
+
+ {messages.howToUnlock}
+
+
+
+
+ {messages.description}{' '}
+
+ {coin?.ticker ? `$${coin.ticker}` : "the artist's coins"}
+
+ .
+
+
+
+
+
+ )
+}
diff --git a/packages/mobile/src/components/locked-text-post-drawer/index.ts b/packages/mobile/src/components/locked-text-post-drawer/index.ts
new file mode 100644
index 00000000000..961211e50eb
--- /dev/null
+++ b/packages/mobile/src/components/locked-text-post-drawer/index.ts
@@ -0,0 +1 @@
+export { LockedTextPostDrawer } from './LockedTextPostDrawer'
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 244f687b36b..cd7ed8681a5 100644
--- a/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx
+++ b/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx
@@ -277,7 +277,11 @@ const FanClubFeed = ({ mint }: { mint: string }) => {
{hasTextPosts
? textPosts.map((item) => (
-
+
))
: null}
@@ -377,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 d8037376ae5..dbcfe5b5a3b 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,8 @@
+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,
@@ -10,44 +10,29 @@ import {
IconLock,
Paper,
Skeleton,
- Text,
- useTheme
+ Text
} from '@audius/harmony-native'
import { ProfilePicture } from 'app/components/core'
import { UserLink } from 'app/components/user-link'
+import { useDrawer } from 'app/hooks/useDrawer'
const messages = {
unlock: 'Unlock',
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
}
-export const TextPostCard = ({ commentId }: TextPostCardProps) => {
+export const TextPostCard = ({ commentId, mint }: TextPostCardProps) => {
const { data: comment, isPending } = useComment(commentId)
- const { color } = useTheme()
+ const { onOpen: openLockedTextPostDrawer } = useDrawer('LockedTextPost')
+
+ const handleUnlock = useCallback(() => {
+ openLockedTextPostDrawer({ mint })
+ }, [openLockedTextPostDrawer, mint])
if (isPending) {
return (
@@ -100,34 +85,21 @@ export const TextPostCard = ({ commentId }: TextPostCardProps) => {
{isLocked ? (
-
-
-
- {generatePlaceholder(commentId)}
+
+
+
+
+
+ {messages.membersOnly}
-
-
-
-
-
-
-
- {messages.membersOnly}
-
-
) : (
diff --git a/packages/mobile/src/store/drawers/slice.ts b/packages/mobile/src/store/drawers/slice.ts
index 6eded2d68dd..cbafaccc266 100644
--- a/packages/mobile/src/store/drawers/slice.ts
+++ b/packages/mobile/src/store/drawers/slice.ts
@@ -35,6 +35,7 @@ export type Drawer =
| 'PickWinners'
| 'CoinInsightsOverflowMenu'
| 'WalletRowOverflowMenu'
+ | 'LockedTextPost'
export type DrawerData = {
EnablePushNotifications: undefined
@@ -77,6 +78,7 @@ export type DrawerData = {
Comment: { userId: ID; entityId: ID; isEntityOwner: boolean; artistId: ID }
EditTrackFormOverflowMenu: undefined
PickWinners: undefined
+ LockedTextPost: { mint: string }
CoinInsightsOverflowMenu: { mint: string }
WalletRowOverflowMenu: {
address: string
@@ -117,6 +119,7 @@ const initialState: DrawersState = {
Comment: false,
EditTrackFormOverflowMenu: false,
PickWinners: false,
+ LockedTextPost: false,
CoinInsightsOverflowMenu: false,
WalletRowOverflowMenu: false,
data: {}
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..2560a00d532
--- /dev/null
+++ b/packages/web/src/pages/fan-club-detail-page/components/LockedTextPostModal.tsx
@@ -0,0 +1,74 @@
+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 d90cad695e2..e84dc0bfad4 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',
@@ -74,6 +76,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
@@ -196,10 +199,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}
@@ -284,6 +294,12 @@ export const TextPostCard = ({ commentId, mint }: TextPostCardProps) => {
onConfirm={handleDelete}
onClose={() => setShowDeleteConfirm(false)}
/>
+
+ setShowUnlockModal(false)}
+ mint={mint}
+ />
)
}