- {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..e07cd7f60 100644
--- a/packages/dapp/src/app/api/campaigns/issue/route.ts
+++ b/packages/dapp/src/app/api/campaigns/issue/route.ts
@@ -59,40 +59,13 @@ export async function POST(request: NextRequest) {
});
}
- const agent = await getAgent();
- const didResolution = await agent.resolveDid({ didUrl: did });
-
- if (
- !didResolution.didDocument ||
- !didResolution.didDocument.verificationMethod
- ) {
- return new NextResponse('Error resolving did', {
- status: 400,
- headers: {
- ...CORS_HEADERS,
- },
- });
- }
-
- if (
- didResolution.didDocument.verificationMethod[0].blockchainAccountId
- ?.split(':')[2]
- .toLowerCase() !== user.address.toLowerCase()
- ) {
- return new NextResponse('Unauthorized', {
- status: 401,
- headers: {
- ...CORS_HEADERS,
- },
- });
- }
-
const supabase = supabaseServiceRoleClient();
const { data: campaign, error: campaignError } = await supabase
.from('campaigns')
- .select('*')
+ .select('*, requirements(id, *)')
.eq('id', campaignId)
+ .order('created_at', { ascending: false })
.single()
.throwOnError();
@@ -105,15 +78,6 @@ export async function POST(request: NextRequest) {
});
}
- 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('*')
@@ -129,6 +93,89 @@ export async function POST(request: NextRequest) {
});
}
+ let claimDate = new Date().toISOString();
+ let alreadyClaimed = false;
+ if (claim.length > 0) {
+ claimDate = claim[0].claimed_at;
+ alreadyClaimed = true;
+ }
+
+ if (!alreadyClaimed) {
+ const now = new Date();
+ const notYetStarted =
+ campaign.start_date && new Date(campaign.start_date) > now;
+ const alreadyFinished =
+ campaign.end_date && new Date(campaign.end_date) < now;
+ const isCampaignInactive = notYetStarted || alreadyFinished;
+ const isCampaignFullyClaimed =
+ campaign.total && campaign.claimed >= campaign.total;
+
+ if (isCampaignInactive) {
+ return new NextResponse('Campaign is not active', {
+ status: 400,
+ headers: { ...CORS_HEADERS },
+ });
+ }
+
+ if (isCampaignFullyClaimed) {
+ 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', {
+ input_campaign_id: campaignId,
+ input_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 });
+
+ if (
+ !didResolution.didDocument ||
+ !didResolution.didDocument.verificationMethod
+ ) {
+ return new NextResponse('Error resolving did', {
+ status: 400,
+ headers: {
+ ...CORS_HEADERS,
+ },
+ });
+ }
+
+ if (
+ didResolution.didDocument.verificationMethod[0].blockchainAccountId
+ ?.split(':')[2]
+ .toLowerCase() !== user.address.toLowerCase()
+ ) {
+ return new NextResponse('Unauthorized', {
+ status: 401,
+ headers: {
+ ...CORS_HEADERS,
+ },
+ });
+ }
+
const controllerKeyId = 'key-1';
const issuerDid = await agent.didManagerImport({
@@ -145,12 +192,8 @@ export async function POST(request: NextRequest) {
],
});
- let claimDate = new Date().toISOString();
- if (claim.length > 0) {
- claimDate = claim[0].claimed_at;
- }
let credentialId = claim[0]?.credential_id;
- if (claim.length === 0) {
+ if (!alreadyClaimed) {
const { data: insertedClaimData, error: updatedClaimError } =
await supabase
.from('claims')
@@ -173,7 +216,6 @@ export async function POST(request: NextRequest) {
});
}
- console.error('Error updating claim', updatedClaimError);
return new NextResponse('Internal Server Error', {
status: 500,
headers: {
diff --git a/packages/dapp/src/app/api/campaigns/requirements/verify/[id]/route.ts b/packages/dapp/src/app/api/campaigns/requirements/verify/[id]/route.ts
index 966cbf973..5bd633f5f 100644
--- a/packages/dapp/src/app/api/campaigns/requirements/verify/[id]/route.ts
+++ b/packages/dapp/src/app/api/campaigns/requirements/verify/[id]/route.ts
@@ -180,8 +180,15 @@ export async function POST(
let credentials: VerifiableCredential[];
try {
- credentials = presentation.verifiableCredential.map((credential) =>
- normalizeCredential(credential)
+ credentials = presentation.verifiableCredential.reduce(
+ (result, credential) => {
+ const normalized = normalizeCredential(credential);
+ if (normalized?.credentialSubject?.id === did) {
+ result.push(normalized);
+ }
+ return result;
+ },
+ [] as VerifiableCredential[]
);
} catch (error) {
console.error('Error decoding credentials', error);
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/RequirementDisplay.tsx b/packages/dapp/src/components/CampaignsDisplay/RequirementDisplay.tsx
index dd76c6562..ca68e307b 100644
--- a/packages/dapp/src/components/CampaignsDisplay/RequirementDisplay.tsx
+++ b/packages/dapp/src/components/CampaignsDisplay/RequirementDisplay.tsx
@@ -12,6 +12,7 @@ import { useSwitchChain, useVerifyRequirement } from '@/hooks';
import { useAuthStore, useMascaStore, useToastStore } from '@/stores';
import { isError } from '@blockchain-lab-um/masca-connector';
import { shallow } from 'zustand/shallow';
+import type { W3CVerifiableCredential } from '@veramo/core';
type RequirementProps = {
requirement: Tables<'requirements'>;
@@ -24,7 +25,14 @@ export const RequirementDisplay = ({
}: RequirementProps) => {
const t = useTranslations('RequirementDisplay');
- const token = useAuthStore((state) => state.token);
+ const { token, isSignedIn, changeIsSignInModalOpen } = useAuthStore(
+ (state) => ({
+ token: state.token,
+ isSignedIn: state.isSignedIn,
+ changeIsSignInModalOpen: state.changeIsSignInModalOpen,
+ }),
+ shallow
+ );
const { api, did, didMethod, changeDID, changeCurrDIDMethod } = useMascaStore(
(state) => ({
@@ -80,9 +88,31 @@ export const RequirementDisplay = ({
return;
}
- // Create a presentation from all the user's credentials
+ // Create a presentation from all the user's credentials except the polygonid ones
const createPresentationResult = await api.createPresentation({
- vcs: queryCredentialsResult.data.map((queryResult) => queryResult.data),
+ vcs: queryCredentialsResult.data.reduce((acc, queryResult) => {
+ const credential = queryResult.data;
+
+ let issuer = null;
+
+ if (!credential.issuer) return acc;
+
+ if (typeof credential.issuer === 'string') {
+ issuer = credential.issuer;
+ } else if (credential.issuer.id) {
+ issuer = credential.issuer.id;
+ }
+
+ if (!issuer) return acc;
+
+ if (
+ !issuer.includes('did:poylgonid') &&
+ !issuer.includes('did:iden3')
+ ) {
+ acc.push(credential);
+ }
+ return acc;
+ }, [] as W3CVerifiableCredential[]),
proofFormat: 'EthereumEip712Signature2021',
});
@@ -91,6 +121,7 @@ export const RequirementDisplay = ({
return;
}
setStartedVerifying(false);
+
await verifyRequirement({
did: currentDid,
presentation: createPresentationResult.data,
@@ -120,7 +151,9 @@ export const RequirementDisplay = ({