Skip to content

Commit

Permalink
chore: profile follow token gating (#1384)
Browse files Browse the repository at this point in the history
  • Loading branch information
bigint committed Dec 9, 2022
1 parent 1d358d0 commit 9b83b9a
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 54 deletions.
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@headlessui/react": "^1.7.4",
"@heroicons/react": "v1",
"@hookform/resolvers": "^2.9.10",
"@lens-protocol/sdk-gated": "^1.1.2",
"@lexical/code": "0.6.5",
"@lexical/hashtag": "0.6.5",
"@lexical/link": "0.6.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,53 +1,38 @@
import ToggleWithHelper from '@components/Shared/ToggleWithHelper';
import { Card } from '@components/UI/Card';
import { CollectionIcon, UsersIcon } from '@heroicons/react/outline';
import { UsersIcon } from '@heroicons/react/outline';
import type { FC } from 'react';
import { useAccessSettingsStore } from 'src/store/access-settings';

const BasicSettings: FC = () => {
const restricted = useAccessSettingsStore((state) => state.restricted);
const setRestricted = useAccessSettingsStore((state) => state.setRestricted);
const collectToView = useAccessSettingsStore((state) => state.collectToView);
const setCollectToView = useAccessSettingsStore((state) => state.setCollectToView);
const followToView = useAccessSettingsStore((state) => state.followToView);
const setFollowToView = useAccessSettingsStore((state) => state.setFollowToView);

return (
<div className="p-5">
<ToggleWithHelper
on={restricted}
setOn={() => setRestricted(!restricted)}
setOn={() => {
setRestricted(!restricted);
}}
label="Add restrictions on who can view this post"
/>
{restricted && (
<>
<Card className="p-5 mt-5">
<div className="space-y-2">
<div className="flex items-center space-x-2">
<CollectionIcon className="h-4 w-4 text-brand-500" />
<span>Collectors can view</span>
</div>
<ToggleWithHelper
on={collectToView}
setOn={() => setCollectToView(!collectToView)}
label="People need to collect it first to be able to view it"
/>
<Card className="p-5 mt-5">
<div className="space-y-2">
<div className="flex items-center space-x-2">
<UsersIcon className="h-4 w-4 text-brand-500" />
<span>Followers can view</span>
</div>
</Card>
<Card className="p-5 mt-5">
<div className="space-y-2">
<div className="flex items-center space-x-2">
<UsersIcon className="h-4 w-4 text-brand-500" />
<span>Followers can view</span>
</div>
<ToggleWithHelper
on={followToView}
setOn={() => setFollowToView(!followToView)}
label="People need to follow you to be able to view it"
/>
</div>
</Card>
</>
<ToggleWithHelper
on={followToView}
setOn={() => setFollowToView(!followToView)}
label="People need to follow you to be able to view it"
/>
</div>
</Card>
)}
</div>
);
Expand Down
14 changes: 12 additions & 2 deletions apps/web/src/components/Composer/Actions/AccessSettings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@ import { Tooltip } from '@components/UI/Tooltip';
import { LockClosedIcon } from '@heroicons/react/outline';
import isFeatureEnabled from '@lib/isFeatureEnabled';
import { Leafwatch } from '@lib/leafwatch';
import clsx from 'clsx';
import { motion } from 'framer-motion';
import type { FC } from 'react';
import { useState } from 'react';
import { useAccessSettingsStore } from 'src/store/access-settings';
import { useAppStore } from 'src/store/app';
import { PUBLICATION } from 'src/tracking';

import BasicSettings from './BasicSettings';

const AccessSettings: FC = () => {
const currentProfile = useAppStore((state) => state.currentProfile);
const restricted = useAccessSettingsStore((state) => state.restricted);
const hasConditions = useAccessSettingsStore((state) => state.hasConditions);
const reset = useAccessSettingsStore((state) => state.reset);
const [showModal, setShowModal] = useState(false);

if (!isFeatureEnabled('access-settings', currentProfile?.id)) {
Expand All @@ -32,7 +37,7 @@ const AccessSettings: FC = () => {
}}
aria-label="Access"
>
<LockClosedIcon className="h-5 w-5 text-brand" />
<LockClosedIcon className={clsx(restricted ? 'text-green-500' : 'text-brand', 'h-5 w-5')} />
</motion.button>
</Tooltip>
<Modal
Expand All @@ -44,7 +49,12 @@ const AccessSettings: FC = () => {
}
icon={<LockClosedIcon className="w-5 h-5 text-brand" />}
show={showModal}
onClose={() => setShowModal(false)}
onClose={() => {
setShowModal(false);
if (!hasConditions()) {
reset();
}
}}
>
<BasicSettings />
</Modal>
Expand Down
80 changes: 70 additions & 10 deletions apps/web/src/components/Composer/NewPublication.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import useBroadcast from '@components/utils/hooks/useBroadcast';
import type { LensterAttachment, LensterPublication } from '@generated/types';
import type { IGif } from '@giphy/js-types';
import { ChatAlt2Icon, PencilAltIcon } from '@heroicons/react/outline';
import type { EncryptedMetadata, FollowCondition } from '@lens-protocol/sdk-gated';
import { LensEnvironment, LensGatedSDK } from '@lens-protocol/sdk-gated';
import type { AccessConditionOutput } from '@lens-protocol/sdk-gated/dist/graphql/types';
import { $convertFromMarkdownString } from '@lexical/markdown';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import getSignature from '@lib/getSignature';
Expand All @@ -31,10 +34,11 @@ import {
RELAY_ON,
SIGN_WALLET
} from 'data/constants';
import type { CreatePublicCommentRequest } from 'lens';
import type { CreatePublicCommentRequest, MetadataAttributeInput, PublicationMetadataV2Input } from 'lens';
import {
CollectModules,
PublicationMainFocus,
PublicationMetadataDisplayTypes,
ReferenceModules,
useCreateCommentTypedDataMutation,
useCreateCommentViaDispatcherMutation,
Expand All @@ -46,14 +50,15 @@ import dynamic from 'next/dynamic';
import type { FC } from 'react';
import { useEffect, useState } from 'react';
import toast from 'react-hot-toast';
import { useAccessSettingsStore } from 'src/store/access-settings';
import { useAppStore } from 'src/store/app';
import { useCollectModuleStore } from 'src/store/collect-module';
import { usePublicationStore } from 'src/store/publication';
import { useReferenceModuleStore } from 'src/store/reference-module';
import { useTransactionPersistStore } from 'src/store/transaction';
import { COMMENT, POST } from 'src/tracking';
import { v4 as uuid } from 'uuid';
import { useContractWrite, useSignTypedData } from 'wagmi';
import { useContractWrite, useProvider, useSigner, useSignTypedData } from 'wagmi';

import Editor from './Editor';

Expand Down Expand Up @@ -103,11 +108,16 @@ const NewPublication: FC<Props> = ({ publication }) => {
const onlyFollowers = useReferenceModuleStore((state) => state.onlyFollowers);
const degreesOfSeparation = useReferenceModuleStore((state) => state.degreesOfSeparation);

// Access module store
const restricted = useAccessSettingsStore((state) => state.restricted);

// States
const [publicationContentError, setPublicationContentError] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [attachments, setAttachments] = useState<LensterAttachment[]>([]);
const [editor] = useLexicalComposerContext();
const provider = useProvider();
const { data: signer } = useSigner();

const isComment = Boolean(publication);
const isAudioPublication = ALLOWED_AUDIO_TYPES.includes(attachments[0]?.type);
Expand All @@ -122,7 +132,13 @@ const NewPublication: FC<Props> = ({ publication }) => {
if (!isComment) {
setShowNewPostModal(false);
}
Leafwatch.track(isComment ? COMMENT.NEW : POST.NEW);

// Track in simple analytics
if (restricted) {
Leafwatch.track(isComment ? COMMENT.TOKEN_GATED : POST.TOKEN_GATED);
} else {
Leafwatch.track(isComment ? COMMENT.NEW : POST.NEW);
}
};

useEffect(() => {
Expand Down Expand Up @@ -277,6 +293,8 @@ const NewPublication: FC<Props> = ({ publication }) => {
return PublicationMainFocus.Image;
} else if (ALLOWED_VIDEO_TYPES.includes(attachments[0]?.type)) {
return PublicationMainFocus.Video;
} else {
return PublicationMainFocus.TextOnly;
}
} else {
return PublicationMainFocus.TextOnly;
Expand All @@ -301,6 +319,42 @@ const NewPublication: FC<Props> = ({ publication }) => {
return isAudioPublication ? audioPublication.coverMimeType : attachments[0]?.type;
};

const createTokenGatedMetadata = async (metadata: PublicationMetadataV2Input) => {
if (!currentProfile) {
return toast.error(SIGN_WALLET);
}

if (!signer) {
return toast.error(SIGN_WALLET);
}

const tokenGatedSdk = await LensGatedSDK.create({ provider, signer, env: LensEnvironment.Mumbai });
await tokenGatedSdk.connect({
address: currentProfile.ownedBy,
env: LensEnvironment.Mumbai
});

// Condition for gating the content
const followAccessCondition: FollowCondition = { profileId: currentProfile.id };
const accessCondition: AccessConditionOutput = { follow: followAccessCondition };

// Generate the encrypted metadata and upload it to Arweave
const { contentURI } = await tokenGatedSdk.gated.encryptMetadata(
metadata,
currentProfile.id,
accessCondition,
async (data: EncryptedMetadata) => {
return await uploadToArweave(data);
}
);

return contentURI;
};

const createMetadata = async (metadata: PublicationMetadataV2Input) => {
return await uploadToArweave(metadata);
};

const createPublication = async () => {
if (!currentProfile) {
return toast.error(SIGN_WALLET);
Expand Down Expand Up @@ -332,23 +386,23 @@ const NewPublication: FC<Props> = ({ publication }) => {
);
}

const attributes = [
const attributes: MetadataAttributeInput[] = [
{
traitType: 'type',
displayType: 'string',
displayType: PublicationMetadataDisplayTypes.String,
value: getMainContentFocus()?.toLowerCase()
}
];

if (isAudioPublication) {
attributes.push({
traitType: 'author',
displayType: 'string',
displayType: PublicationMetadataDisplayTypes.String,
value: audioPublication.author
});
}

const id = await uploadToArweave({
const metadata: PublicationMetadataV2Input = {
version: '2.0.0',
metadata_id: uuid(),
description: trimify(publicationContent),
Expand All @@ -366,13 +420,19 @@ const NewPublication: FC<Props> = ({ publication }) => {
attributes,
media: attachments,
locale: getUserLocale(),
createdOn: new Date(),
appId: APP_NAME
});
};

let arweaveId = null;
if (restricted) {
arweaveId = await createTokenGatedMetadata(metadata);
} else {
arweaveId = await createMetadata(metadata);
}

const request = {
profileId: currentProfile?.id,
contentURI: `https://arweave.net/${id}`,
contentURI: `https://arweave.net/${arweaveId}`,
...(isComment && {
publicationId: publication.__typename === 'Mirror' ? publication?.mirrorOf?.id : publication?.id
}),
Expand Down
13 changes: 7 additions & 6 deletions apps/web/src/store/access-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@ import create from 'zustand';
interface AccessSettingsState {
restricted: boolean;
setRestricted: (restricted: boolean) => void;
collectToView: boolean;
setCollectToView: (collectToView: boolean) => void;
followToView: boolean;
setFollowToView: (followToView: boolean) => void;
hasConditions: () => boolean;
reset: () => void;
}

export const useAccessSettingsStore = create<AccessSettingsState>((set) => ({
export const useAccessSettingsStore = create<AccessSettingsState>((set, get) => ({
restricted: false,
setRestricted: (restricted) => set(() => ({ restricted })),
collectToView: false,
setCollectToView: (collectToView) => set(() => ({ collectToView })),
followToView: false,
setFollowToView: (followToView) => set(() => ({ followToView })),
hasConditions: () => {
const { followToView } = get();

return followToView;
},
reset: () =>
set(() => ({
restricted: false,
collectToView: false,
followToView: false
}))
}));
6 changes: 4 additions & 2 deletions apps/web/src/tracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,13 @@ export const PUBLICATION = {
};

export const POST = {
NEW: 'new_post'
NEW: 'new_post',
TOKEN_GATED: 'new_token_gated_post'
};

export const COMMENT = {
NEW: 'new_comment'
NEW: 'new_comment',
TOKEN_GATED: 'new_token_gated_comment'
};

export const NOTIFICATION = {
Expand Down
3 changes: 2 additions & 1 deletion packages/data/feature-flags.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { aaveMembers } from './aave-members';
import { lensterMembers } from './lenster-members';

export const featureFlags = [
Expand All @@ -9,6 +10,6 @@ export const featureFlags = [
{
key: 'access-settings',
name: 'Access settings',
enabledFor: [...lensterMembers]
enabledFor: [...lensterMembers, ...aaveMembers]
}
];
Loading

3 comments on commit 9b83b9a

@vercel
Copy link

@vercel vercel bot commented on 9b83b9a Dec 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

pro – ./apps/pro

lenster-pro.vercel.app
pro.lenster.xyz
pro-git-main-lenster.vercel.app
pro-lenster.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 9b83b9a Dec 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

api – ./apps/api

lenster-api.vercel.app
api-git-main-lenster.vercel.app
api.lenster.xyz
api-lenster.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 9b83b9a Dec 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

web – ./apps/web

web-lenster.vercel.app
lenster.vercel.app
web-git-main-lenster.vercel.app
lenster.xyz

Please sign in to comment.