Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: profile follow token gating #1384

Merged
merged 15 commits into from
Dec 9, 2022
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
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