Skip to content

Commit

Permalink
Feat: Registration Discount codes (#1084)
Browse files Browse the repository at this point in the history
* discount code temp

* comments

* BE: Proof of concept

* mark/increment code as consumed

* bad code

* bad code

* dry it up

* remove test code

* cleanup and fix

* suspense

* undo change

* refactor: use internal transaction/capacities hooks for registration flow

* refetch basename

* fix: wrong name being used when reverseRecord is false

* prefetch

* ssr code instead of search params

* add banner & code
  • Loading branch information
kirkas authored Oct 21, 2024
1 parent c38fcab commit 7dcd856
Show file tree
Hide file tree
Showing 17 changed files with 570 additions and 274 deletions.
10 changes: 8 additions & 2 deletions apps/web/app/(basenames)/names/RegistrationProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import RegistrationProvider from 'apps/web/src/components/Basenames/Registration

const usernameRegistrationAnalyticContext = 'username_registration';

export default function RegistrationProviders({ children }: { children: React.ReactNode }) {
export default function RegistrationProviders({
children,
code,
}: {
children: React.ReactNode;
code?: string;
}) {
return (
<AnalyticsProvider context={usernameRegistrationAnalyticContext}>
<RegistrationProvider>{children}</RegistrationProvider>
<RegistrationProvider code={code}>{children}</RegistrationProvider>
</AnalyticsProvider>
);
}
22 changes: 11 additions & 11 deletions apps/web/app/(basenames)/names/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import RegistrationFAQ from 'apps/web/src/components/Basenames/RegistrationFaq';
import RegistrationFlow from 'apps/web/src/components/Basenames/RegistrationFlow';
import RegistrationValueProp from 'apps/web/src/components/Basenames/RegistrationValueProp';
import type { Metadata } from 'next';
import { Suspense } from 'react';
import basenameCover from './basename_cover.png';
import { initialFrame } from 'apps/web/pages/api/basenames/frame/frameResponses';

Expand All @@ -28,18 +27,19 @@ export const metadata: Metadata = {
},
};

export default async function Page() {
type PageProps = { searchParams?: { code?: string } };
export default async function Page({ searchParams }: PageProps) {
const code = searchParams?.code;

return (
<ErrorsProvider context="registration">
<RegistrationProviders>
<Suspense>
<main>
<RegistrationFlow />
<RegistrationValueProp />
<PoweredByEns />
<RegistrationFAQ />
</main>
</Suspense>
<RegistrationProviders code={code}>
<main>
<RegistrationFlow />
<RegistrationValueProp />
<PoweredByEns />
<RegistrationFAQ />
</main>
</RegistrationProviders>
</ErrorsProvider>
);
Expand Down
36 changes: 36 additions & 0 deletions apps/web/pages/api/proofs/discountCode/consume/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { logger } from 'apps/web/src/utils/logger';
import { withTimeout } from 'apps/web/pages/api/decorators';
import { incrementDiscountCodeUsage } from 'apps/web/src/utils/proofs/discount_code_storage';

/*
this endpoint will increment the discount code usage to prevent abuse
*/

type DiscountCodeRequest = {
code: string;
};

async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}

try {
const { code } = req.body as DiscountCodeRequest;

if (!code || typeof code !== 'string') {
return res.status(500).json({ error: 'Invalid request' });
}

await incrementDiscountCodeUsage(code);

return res.status(200).json({ success: true });
} catch (error: unknown) {
logger.error('error incrementing the discount code', error);
}
// If error is not an instance of Error, return a generic error message
return res.status(500).json({ error: 'An unexpected error occurred' });
}

export default withTimeout(handler);
81 changes: 81 additions & 0 deletions apps/web/pages/api/proofs/discountCode/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { proofValidation, signDiscountMessageWithTrustedSigner } from 'apps/web/src/utils/proofs';
import { logger } from 'apps/web/src/utils/logger';
import { withTimeout } from 'apps/web/pages/api/decorators';
import { Address, Hash, stringToHex } from 'viem';
import { USERNAME_DISCOUNT_CODE_VALIDATORS } from 'apps/web/src/addresses/usernames';
import { baseSepolia } from 'viem/chains';
import { getDiscountCode } from 'apps/web/src/utils/proofs/discount_code_storage';

export type DiscountCodeResponse = {
discountValidatorAddress: Address;
address: Address;
signedMessage: Hash;
};

/*
this endpoint returns whether or a discount code is valid
*/
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'method not allowed' });
}
const { address, chain, code } = req.query;
const validationErr = proofValidation(address, chain);
if (validationErr) {
return res.status(validationErr.status).json({ error: validationErr.error });
}

if (!code || typeof code !== 'string') {
return res.status(500).json({ error: 'Discount code invalid' });
}

try {
// 1. get the database model
const discountCodes = await getDiscountCode(code);

// 2. Validation: Coupon exists
if (!discountCodes || discountCodes.length === 0) {
return res.status(500).json({ error: 'Discount code invalid' });
}

const discountCode = discountCodes[0];

// 2.1 Validation: Coupon is expired
if (new Date(discountCode.expires_at) < new Date()) {
return res.status(500).json({ error: 'Discount code invalid' });
}

// 2.2 Validation: Coupon can be redeemed
if (Number(discountCode.usage_count) >= Number(discountCode.usage_limit)) {
return res.status(500).json({ error: 'Discount code invalid' });
}

// 3. Sign the validationData
const couponCodeUuid = stringToHex(discountCode.code, { size: 32 });
const expirationTimeUnix = Math.floor(discountCode.expires_at.getTime() / 1000);

const signature = await signDiscountMessageWithTrustedSigner(
address as Address,
couponCodeUuid,
// TODO: Set variable chain
USERNAME_DISCOUNT_CODE_VALIDATORS[baseSepolia.id],
expirationTimeUnix,
);

// 4. Return the discount data
const result: DiscountCodeResponse = {
discountValidatorAddress: USERNAME_DISCOUNT_CODE_VALIDATORS[baseSepolia.id],
address: address as Address,
signedMessage: signature,
};

return res.status(200).json(result);
} catch (error: unknown) {
logger.error('error getting proofs for discount code', error);
}
// If error is not an instance of Error, return a generic error message
return res.status(500).json({ error: 'An unexpected error occurred' });
}

export default withTimeout(handler);
5 changes: 5 additions & 0 deletions apps/web/src/addresses/usernames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,8 @@ export const EXPONENTIAL_PREMIUM_PRICE_ORACLE: AddressMap = {
[baseSepolia.id]: '0x2B73408052825e17e0Fe464f92De85e8c7723231',
[base.id]: '0xd53B558e1F07289acedf028d226974AbBa258312',
};

export const USERNAME_DISCOUNT_CODE_VALIDATORS: AddressMap = {
[baseSepolia.id]: '0x52acEeB464F600437a3681bEC087fb53F3f75638',
[base.id]: '0x6F9A31238F502E9C9489274E59a44c967F4deC91',
};
Loading

0 comments on commit 7dcd856

Please sign in to comment.