diff --git a/.changeset/five-poets-yell.md b/.changeset/five-poets-yell.md new file mode 100644 index 000000000..2a50ed7ca --- /dev/null +++ b/.changeset/five-poets-yell.md @@ -0,0 +1,5 @@ +--- +"@blockchain-lab-um/dapp": patch +--- + +Adds reclaim option for campaigns. diff --git a/packages/dapp/src/app/[locale]/(public)/ecosystem/page.tsx b/packages/dapp/src/app/[locale]/(public)/ecosystem/page.tsx index bd389d73a..6fbbb5087 100644 --- a/packages/dapp/src/app/[locale]/(public)/ecosystem/page.tsx +++ b/packages/dapp/src/app/[locale]/(public)/ecosystem/page.tsx @@ -80,7 +80,7 @@ const projectsDark: ProjectIconProps[] = [ export default function Page() { return ( -
+

Applications diff --git a/packages/dapp/src/app/[locale]/(public)/layout.tsx b/packages/dapp/src/app/[locale]/(public)/layout.tsx index a2bcc5996..f50522c79 100644 --- a/packages/dapp/src/app/[locale]/(public)/layout.tsx +++ b/packages/dapp/src/app/[locale]/(public)/layout.tsx @@ -7,8 +7,10 @@ export default async function PublicLayout({ children: React.ReactNode; }) { return ( -
- +
+
+ +
{children} diff --git a/packages/dapp/src/app/[locale]/app/(protected)/settings/page.tsx b/packages/dapp/src/app/[locale]/app/(protected)/settings/page.tsx index d399f8beb..ba2d3ea18 100644 --- a/packages/dapp/src/app/[locale]/app/(protected)/settings/page.tsx +++ b/packages/dapp/src/app/[locale]/app/(protected)/settings/page.tsx @@ -9,7 +9,7 @@ export const metadata: Metadata = { export default function Page() { return ( -
+
diff --git a/packages/dapp/src/app/[locale]/app/(public)/campaigns/page.tsx b/packages/dapp/src/app/[locale]/app/(public)/campaigns/page.tsx index a1e0f24b3..600bee2b4 100644 --- a/packages/dapp/src/app/[locale]/app/(public)/campaigns/page.tsx +++ b/packages/dapp/src/app/[locale]/app/(public)/campaigns/page.tsx @@ -8,7 +8,7 @@ export const metadata: Metadata = { export default function Page() { return ( -
+
); diff --git a/packages/dapp/src/app/[locale]/app/layout.tsx b/packages/dapp/src/app/[locale]/app/layout.tsx index 8cb014258..94aa900d3 100644 --- a/packages/dapp/src/app/[locale]/app/layout.tsx +++ b/packages/dapp/src/app/[locale]/app/layout.tsx @@ -5,6 +5,7 @@ import AppNavbar from '@/components/AppNavbar'; import { SignInModal } from '@/components/SignInModal'; import ToastWrapper from '@/components/ToastWrapper'; import { Providers } from '@/components/Providers'; +import { ScrollShadow } from '@nextui-org/react'; export default async function AppLayout({ children, @@ -13,16 +14,17 @@ export default async function AppLayout({ }) { return ( - -
-
- {children} +
+
+
+ + {children} +
diff --git a/packages/dapp/src/app/api/campaigns/claims/route.ts b/packages/dapp/src/app/api/campaigns/claims/route.ts new file mode 100644 index 000000000..a393bc8d3 --- /dev/null +++ b/packages/dapp/src/app/api/campaigns/claims/route.ts @@ -0,0 +1,58 @@ +import { type NextRequest, NextResponse } from 'next/server'; +import jwt from 'jsonwebtoken'; +import { supabaseServiceRoleClient } from '@/utils/supabase/supabaseServiceRoleClient'; +import { supabaseClient } from '@/utils/supabase/supabaseClient'; + +const CORS_HEADERS = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', +}; + +export const dynamic = 'force-dynamic'; + +export async function GET(request: NextRequest) { + const token = request.headers.get('Authorization')?.replace('Bearer ', ''); + if (!token) { + return new NextResponse('Unauthorized', { + status: 401, + headers: { + ...CORS_HEADERS, + }, + }); + } + const user = jwt.verify(token, process.env.SUPABASE_JWT_SECRET!) as { + sub: string; + address: string; + aud: string; + role: string; + iat: number; + exp: number; + }; + + const supabase = supabaseServiceRoleClient(); + const { data: userId } = await supabase + .from('users') + .select('id') + .eq('address', user.address.toLowerCase()) + .single(); + if (!userId) { + return NextResponse.json({ claims: [] }, { headers: { ...CORS_HEADERS } }); + } + const { data: claims, error } = await supabase + .from('claims') + .select('*') + .eq('user_id', userId.id); + + if (error) { + console.error('Error getting claims', error); + return new NextResponse('Error getting claims', { + status: 500, + headers: { + ...CORS_HEADERS, + }, + }); + } + + return NextResponse.json({ claims }, { headers: { ...CORS_HEADERS } }); +} diff --git a/packages/dapp/src/app/api/campaigns/issue/route.ts b/packages/dapp/src/app/api/campaigns/issue/route.ts index a2c9cd04e..4ebd7fb38 100644 --- a/packages/dapp/src/app/api/campaigns/issue/route.ts +++ b/packages/dapp/src/app/api/campaigns/issue/route.ts @@ -58,6 +58,54 @@ export async function POST(request: NextRequest) { }, }); } + const supabase = supabaseServiceRoleClient(); + const { data: campaign, error: campaignError } = await supabase + .from('campaigns') + .select('*, requirements(id, *)') + .eq('id', campaignId) + .order('created_at', { ascending: false }) + .single() + .throwOnError(); + + if (campaignError) { + return new NextResponse('Campaign not found', { + status: 404, + headers: { + ...CORS_HEADERS, + }, + }); + } + + if (campaign.total && campaign.claimed >= campaign.total) { + return new NextResponse('Campaign is already fully claimed', { + status: 400, + headers: { + ...CORS_HEADERS, + }, + }); + } + const { data: completedRequirements, error: completedRequirementsError } = + await supabase.rpc('get_num_of_users_requirements_by_campaign', { + campaign_id: campaignId, + user_id: user.sub, + }); + + if (completedRequirementsError) { + return new NextResponse('Internal Server Error', { + status: 500, + headers: { + ...CORS_HEADERS, + }, + }); + } + if (completedRequirements !== campaign.requirements.length) { + return new NextResponse('User has not completed all requirements', { + status: 400, + headers: { + ...CORS_HEADERS, + }, + }); + } const agent = await getAgent(); const didResolution = await agent.resolveDid({ didUrl: did }); @@ -87,33 +135,6 @@ export async function POST(request: NextRequest) { }); } - const supabase = supabaseServiceRoleClient(); - - const { data: campaign, error: campaignError } = await supabase - .from('campaigns') - .select('*') - .eq('id', campaignId) - .single() - .throwOnError(); - - if (campaignError) { - return new NextResponse('Campaign not found', { - status: 404, - headers: { - ...CORS_HEADERS, - }, - }); - } - - if (campaign.total && campaign.claimed >= campaign.total) { - return new NextResponse('Campaign is already fully claimed', { - status: 400, - headers: { - ...CORS_HEADERS, - }, - }); - } - const { data: claim, error: claimError } = await supabase .from('claims') .select('*') diff --git a/packages/dapp/src/components/AppNavbar/index.tsx b/packages/dapp/src/components/AppNavbar/index.tsx index 349de074d..96d945548 100644 --- a/packages/dapp/src/components/AppNavbar/index.tsx +++ b/packages/dapp/src/components/AppNavbar/index.tsx @@ -35,8 +35,8 @@ export default function AppNavbar() { const { isConnected } = useAccount(); return ( -
-
+
+
@@ -45,7 +45,7 @@ export default function AppNavbar() {

-
+
{MAIN_LINKS.map(({ name, href, requiresConnection }) => { if ((requiresConnection && isConnected) || !requiresConnection) { return ( diff --git a/packages/dapp/src/components/AuthorizationRequest/index.tsx b/packages/dapp/src/components/AuthorizationRequest/index.tsx index 400489cf0..703c97a1f 100644 --- a/packages/dapp/src/components/AuthorizationRequest/index.tsx +++ b/packages/dapp/src/components/AuthorizationRequest/index.tsx @@ -112,14 +112,6 @@ const AuthorizationRequestFlow = () => { } }; - const sendAuthorizationResponse = async () => { - if (!api || !authorizationRequestURI || !parsedAuthorizationRequestURI) { - return; - } - - console.log('here'); - }; - useEffect(() => { if (!credentials.length) return; setIsSelectModalOpen(true); @@ -127,8 +119,6 @@ const AuthorizationRequestFlow = () => { useEffect(() => { if (!selectedCredentials.length) return; - - console.log(selectedCredentials); // TODO: // sendAuthorizationResponse().catch((e) => console.log(e)); }, [selectedCredentials]); diff --git a/packages/dapp/src/components/CampaignsDisplay/CampaignDisplay.tsx b/packages/dapp/src/components/CampaignsDisplay/CampaignDisplay.tsx index 1ba73ffef..5182f05b7 100644 --- a/packages/dapp/src/components/CampaignsDisplay/CampaignDisplay.tsx +++ b/packages/dapp/src/components/CampaignsDisplay/CampaignDisplay.tsx @@ -14,9 +14,11 @@ import { useSwitchChain, } from '@/hooks'; import { useAccount } from 'wagmi'; +import { RewardDisplay } from './RewardDisplay'; type CampaignProps = { campaign: Campaigns[number]; + alreadyClaimed: boolean; }; export const CampaignDisplay = ({ @@ -28,7 +30,9 @@ export const CampaignDisplay = ({ total, image_url: imageUrl, requirements, + rewards, }, + alreadyClaimed, }: CampaignProps) => { const t = useTranslations('CampaignDisplay'); @@ -122,6 +126,12 @@ export const CampaignDisplay = ({

{description}

+
+ {t('rewards')} +
+
+ +
{requirements.length > 0 && (
{t('requirements')} @@ -142,13 +152,27 @@ export const CampaignDisplay = ({
diff --git a/packages/dapp/src/components/CampaignsDisplay/RewardDisplay.tsx b/packages/dapp/src/components/CampaignsDisplay/RewardDisplay.tsx new file mode 100644 index 000000000..9559eaf33 --- /dev/null +++ b/packages/dapp/src/components/CampaignsDisplay/RewardDisplay.tsx @@ -0,0 +1,17 @@ +import { Chip } from '@nextui-org/react'; + +type RequirementProps = { + reward: string; +}; + +export const RewardDisplay = ({ reward }: RequirementProps) => { + return ( +
+ +

+ {reward} +

+
+
+ ); +}; diff --git a/packages/dapp/src/components/CampaignsDisplay/index.tsx b/packages/dapp/src/components/CampaignsDisplay/index.tsx index 30e3ebb69..509d59df4 100644 --- a/packages/dapp/src/components/CampaignsDisplay/index.tsx +++ b/packages/dapp/src/components/CampaignsDisplay/index.tsx @@ -5,10 +5,28 @@ import { CampaignDisplay } from './CampaignDisplay'; import { useCampaigns } from '@/hooks'; import { Spinner } from '@nextui-org/react'; import { useTranslations } from 'next-intl'; +import { useAuthStore } from '@/stores'; +import { shallow } from 'zustand/shallow'; +import { useCampaignClaims } from '@/hooks/useCampaignClaims'; export const CampaignsDisplay = () => { const t = useTranslations('CampaignsDisplay'); + const { data, status } = useCampaigns(); + const campaigns = data?.campaigns || []; + const { token } = useAuthStore( + (state) => ({ + token: state.token, + isSignedIn: state.isSignedIn, + changeIsSignInModalOpen: state.changeIsSignInModalOpen, + }), + shallow + ); + const { data: claimsData } = useCampaignClaims(token); + + if (campaigns.length === 0) { + return
{t('no-campaigns')}
; + } if (status === 'pending') { return ( @@ -18,16 +36,26 @@ export const CampaignsDisplay = () => { ); } - const campaigns = data?.campaigns || []; - - if (campaigns.length === 0) { - return
{t('no-campaigns')}
; - } - return ( -
+
+
+
+ {t('title')} +
+

{t('description')}

+
{campaigns.map((campaign) => ( - +
+ claim.campaign_id === campaign.id + ) + } + /> +
))}
); diff --git a/packages/dapp/src/components/ConnectedProvider/index.tsx b/packages/dapp/src/components/ConnectedProvider/index.tsx index 8c52f9738..3c97a8787 100644 --- a/packages/dapp/src/components/ConnectedProvider/index.tsx +++ b/packages/dapp/src/components/ConnectedProvider/index.tsx @@ -22,9 +22,9 @@ const ConnectedProvider = ({ children }: ConnectedProviderProps) => { return isConnected ? ( <>{children} ) : ( -
-
-
+
+
+

{t('connect')}

@@ -32,16 +32,16 @@ const ConnectedProvider = ({ children }: ConnectedProviderProps) => { {t('version')}
-
+
-
+
{t('masca')}
-
+
{t('masca-desc')}
@@ -49,14 +49,14 @@ const ConnectedProvider = ({ children }: ConnectedProviderProps) => {
    -
  • +
  • {t('features.feat-1')}
    -
    +
    {t('features.desc-1-1')} {t('features.desc-1-2')} @@ -68,14 +68,14 @@ const ConnectedProvider = ({ children }: ConnectedProviderProps) => { .
  • -
  • +
  • {t('features.feat-2')}
    -
    +
    {t('features.desc-2-1')} {t('features.desc-2-2')} @@ -87,14 +87,14 @@ const ConnectedProvider = ({ children }: ConnectedProviderProps) => { .
  • -
  • +
  • {t('features.feat-3')}
    -
    +
    {t('features.desc-3-1')} {t('features.desc-3-2')} diff --git a/packages/dapp/src/components/CredentialDisplay/index.tsx b/packages/dapp/src/components/CredentialDisplay/index.tsx index 26bc13f32..0af86bf24 100644 --- a/packages/dapp/src/components/CredentialDisplay/index.tsx +++ b/packages/dapp/src/components/CredentialDisplay/index.tsx @@ -19,7 +19,10 @@ import { useAuthStore, useShareModalStore, } from '@/stores'; -import { removeCredentialSubjectFilterString } from '@/utils/format'; +import { + capitalizeString, + removeCredentialSubjectFilterString, +} from '@/utils/format'; import Button from '../Button'; import DeleteModal from '../DeleteModal'; import ModifyDSModal from '../ModifyDSModal'; @@ -152,7 +155,7 @@ const CredentialDisplay = ({ id }: CredentialDisplayProps) => {
    {vc.metadata.store.map((store) => ( diff --git a/packages/dapp/src/components/DashboardDisplay/CredentialTable/index.tsx b/packages/dapp/src/components/DashboardDisplay/CredentialTable/index.tsx index 967ba7479..540316044 100644 --- a/packages/dapp/src/components/DashboardDisplay/CredentialTable/index.tsx +++ b/packages/dapp/src/components/DashboardDisplay/CredentialTable/index.tsx @@ -26,7 +26,11 @@ import React, { useEffect, useMemo, useState } from 'react'; import DeleteModal from '@/components/DeleteModal'; import StoreIcon from '@/components/StoreIcon'; import { useTableStore, useAuthStore, useShareModalStore } from '@/stores'; -import { formatDid, removeCredentialSubjectFilterString } from '@/utils/format'; +import { + capitalizeString, + formatDid, + removeCredentialSubjectFilterString, +} from '@/utils/format'; import { convertTypes } from '@/utils/string'; import { LastFetched } from '../LastFetched'; import { sortCredentialList } from '../utils'; @@ -214,7 +218,7 @@ const CredentialTable = ({ vcs }: CredentialTableProps) => { {dataStore.split(',').map((store: string) => (
    diff --git a/packages/dapp/src/components/DeleteModal/index.tsx b/packages/dapp/src/components/DeleteModal/index.tsx index 63debcb29..4361b2314 100644 --- a/packages/dapp/src/components/DeleteModal/index.tsx +++ b/packages/dapp/src/components/DeleteModal/index.tsx @@ -8,7 +8,7 @@ import { useTranslations } from 'next-intl'; import Button from '@/components/Button'; import { useMascaStore, useToastStore } from '@/stores'; -import { stringifyCredentialSubject } from '@/utils/format'; +import { capitalizeString, stringifyCredentialSubject } from '@/utils/format'; interface DeleteModalProps { isOpen: boolean; @@ -32,7 +32,7 @@ function DeleteModal({ isOpen, setOpen, vc, store }: DeleteModalProps) { setTimeout(() => { useToastStore.setState({ open: true, - title: t('deleting'), + title: t('deleting-loading'), type: 'normal', loading: true, link: null, @@ -124,9 +124,9 @@ function DeleteModal({ isOpen, setOpen, vc, store }: DeleteModalProps) { {store && (

    - {t('deleting')}:{' '} + {t('deleting')} - {store} + {capitalizeString(store)}

    )} diff --git a/packages/dapp/src/components/LandingPage/index.tsx b/packages/dapp/src/components/LandingPage/index.tsx index b9907f67e..8bb9e3afc 100644 --- a/packages/dapp/src/components/LandingPage/index.tsx +++ b/packages/dapp/src/components/LandingPage/index.tsx @@ -10,7 +10,7 @@ const LandingPage = () => { const t = useTranslations('Home'); return ( -
    +
    {t('title-1')} diff --git a/packages/dapp/src/components/MetaMaskProvider/index.tsx b/packages/dapp/src/components/MetaMaskProvider/index.tsx index b0c40166a..88bf53f69 100644 --- a/packages/dapp/src/components/MetaMaskProvider/index.tsx +++ b/packages/dapp/src/components/MetaMaskProvider/index.tsx @@ -35,7 +35,7 @@ const MetaMaskProvider = ({ children }: MetaMaskProviderProps) => { return !hasMetamask && !isConnected ? (
    -
    +

    {t('metamask')}

    diff --git a/packages/dapp/src/components/ModifyDSModal/index.tsx b/packages/dapp/src/components/ModifyDSModal/index.tsx index d7806cc31..41654c077 100644 --- a/packages/dapp/src/components/ModifyDSModal/index.tsx +++ b/packages/dapp/src/components/ModifyDSModal/index.tsx @@ -12,7 +12,7 @@ import ToggleSwitch from '@/components//Switch'; import DeleteModal from '@/components/DeleteModal'; import { useMascaStore, useToastStore } from '@/stores'; import { isPolygonVC } from '@/utils/credential'; -import { stringifyCredentialSubject } from '@/utils/format'; +import { capitalizeString, stringifyCredentialSubject } from '@/utils/format'; interface ModifyDSModalProps { isOpen: boolean; @@ -161,7 +161,7 @@ function ModifyDSModal({ isOpen, setOpen, vc }: ModifyDSModalProps) { key={store} className="mt-3 flex items-center justify-between" > -
    {store}
    +
    {capitalizeString(store)}
    { const pathname = usePathname() ?? '/'; return ( -
    -
    +
    +
    diff --git a/packages/dapp/src/components/SettingsCard/index.tsx b/packages/dapp/src/components/SettingsCard/index.tsx index 040e93c2f..b4c550492 100644 --- a/packages/dapp/src/components/SettingsCard/index.tsx +++ b/packages/dapp/src/components/SettingsCard/index.tsx @@ -192,7 +192,7 @@ const SettingsCard = () => { return (
    -
    +