Skip to content

Commit

Permalink
Merge pull request #292 from OfflineHQ/286-unlock-4-implement-key-loy…
Browse files Browse the repository at this point in the history
…alty-card-contract

286 unlock 4 implement key loyalty card contract
  • Loading branch information
sebpalluel authored Apr 22, 2024
2 parents 63dc5b8 + 45297fa commit 4899d13
Show file tree
Hide file tree
Showing 804 changed files with 186,395 additions and 96,086 deletions.
18 changes: 18 additions & 0 deletions .cursorrules
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Project Context

- Name: Offline - Next-gen Consumer Brand Interaction
- Description: At Offline, we are developing innovative solutions to enhance brand-customer interactions through user-centric applications using blockchain and NFT technologies. The project consists of three main apps: the web platform for users, a back-office for companies and organizers, and a micro-app called 'unlock' for token gating on Shopify pages and potential future vendor integrations.

# Technology Stack

- Frontend: Next.js 14, Storybook for component development
- Backend: Hasura/Postgres for database operations
- Testing: Jest for unit and integration tests; Playwright for end-to-end testing
- Architecture: Nx monorepo setup with clean domain models. Libraries are organized within the 'libs' folder.

# Code Standards

- Clean Code: Follow SOLID principles and ensure code is DRY and easily testable.
- Typing: Employ strict TypeScript typing to ensure robustness and maintainability.
- Testing: Aim for high test coverage with well-thought-out unit, integration, and E2E tests.
- Documentation: Maintain clear and comprehensive documentation, especially for public APIs and complex logic.
25 changes: 16 additions & 9 deletions .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ NX_VERCEL_REMOTE_CACHE_TOKEN=8HfZAvj1ZCUEYLuNISczuuF2
NX_VERCEL_REMOTE_CACHE_TEAM=team_cNOd6z2HTsiaB0cibAjaQSIM

# hasura
HASURA_VERSION=v2.35.0
HASURA_VERSION=v2.38.0
HASURA_GRAPHQL_SERVER_PORT=8080
HASURA_GRAPHQL_ADMIN_SECRET=password
HASURA_CONSOLE_PORT=9695
Expand Down Expand Up @@ -39,13 +39,13 @@ NEXTAUTH_SECRET="-----BEGIN RSA PRIVATE KEY-----\nMIIJKQIBAAKCAgEAtHmpqKfTdp2DNn
TOKEN_LIFE_TIME="648000"

## web3
## Here we take the Mumbai testnet
NEXT_PUBLIC_CHAIN='80001'
CHAIN='80001'
## Here we take the Amoy testnet
NEXT_PUBLIC_CHAIN='80002'
CHAIN='80002'

## Alchemy
NEXT_PUBLIC_ALCHEMY_API_KEY='aTwl7l6SRRC22e-7joQbT7oMG6yTjUm0'
ALCHEMY_API_KEY='aTwl7l6SRRC22e-7joQbT7oMG6yTjUm0'
NEXT_PUBLIC_ALCHEMY_API_KEY='o-0Gv1BvrP4mEFNNQEQziQvc7SI3T7rm'
ALCHEMY_API_KEY='o-0Gv1BvrP4mEFNNQEQziQvc7SI3T7rm'
ALCHEMY_AUTH_TOKEN='dGlcmvJsh6Oac3KFJFok2r0R2UgTrcYm'


Expand All @@ -62,6 +62,10 @@ EMAIL_WEB_UI=8025
WEB_APP_URL='https://www.staging.offline.live/'
NEXT_PUBLIC_WEB_APP_URL='https://www.staging.offline.live/'

## API

API_SECRET_ENCRYPTION_KEY="5557ed70d7c542efa7d9b23516a3baed"

# VIRTUAL_HOST=example.com
# LETSENCRYPT_HOST=example.com
# [email protected]
Expand Down Expand Up @@ -127,9 +131,12 @@ POSTHOG_PERSONAL_API_KEY=phx_wIGzuBaUWMAdzBpxpkil4yIDdINhy5FfnFR4QYhUj6q
NEXT_PUBLIC_POSTHOG_KEY=phc_FcjrV3dP2qG5qVafKh6ULkTYGq5FWD5mT8XPk7t9Y8Q

# Cometh
NEXT_PUBLIC_COMETH_CONNECT_API_KEY=xPVZx4OVf7oR9O2yOI5zTOgkatkkzF6W
COMETH_CONNECT_API_KEY=xPVZx4OVf7oR9O2yOI5zTOgkatkkzF6W
NEXT_PUBLIC_COMETH_CONNECT_API_KEY=vDMJtXRUsdDVCXJ0GBAaKMTjuebI5S3Y
COMETH_CONNECT_API_KEY=vDMJtXRUsdDVCXJ0GBAaKMTjuebI5S3Y

# Wallet connect
NEXT_PUBLIC_WC_PROJECT_ID=68b34422801cb3e8ea1eb7f823266c28
NEXT_PUBLIC_WC_RELAY_URL=wss://relay.walletconnect.com
NEXT_PUBLIC_WC_RELAY_URL=wss://relay.walletconnect.com

# Shopify
SHOPIFY_SHARED_SECRET=c886ebdff67650455049c4cc52517c0d
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
auto-install-peers=false
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async function EventSheetPageContent({
if (!user || !user?.role) return notFound();
const event = await getEventWithPassesOrganizer({ slug: eventSlug, locale });
if (!event) return notFound();
return <EventSheet event={event} organizerId={user.role.organizerId} />;
return <EventSheet event={event} organizerId={user.role?.organizerId} />;
}

interface EventSheetPageProps {
Expand Down
5 changes: 5 additions & 0 deletions apps/back-office/app/[locale]/loyalty-card/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LoyaltyCardSkeleton } from '@features/back-office/loyalty-card';

export default function LoyaltyCardLoading() {
return <LoyaltyCardSkeleton />;
}
16 changes: 16 additions & 0 deletions apps/back-office/app/[locale]/loyalty-card/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { LoyaltyCardPage } from '@features/back-office/loyalty-card';
import { getLoyaltyCardOrganizer } from '@features/back-office/loyalty-card-api';
import { type Locale } from '@next/i18n';

interface LoyaltyCardProps {
params: {
locale: Locale;
};
}

export default async function LoyaltyCard({
params: { locale },
}: LoyaltyCardProps) {
const loyaltyCard = await getLoyaltyCardOrganizer();
return <LoyaltyCardPage loyaltyCard={loyaltyCard} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async function ContentSpaceSheetPageContent({
return (
<ContentSpaceSheet
contentSpace={contentSpace}
organizerId={user.role.organizerId}
organizerId={user.role?.organizerId}
/>
);
}
Expand Down
2 changes: 2 additions & 0 deletions apps/back-office/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { NextRequest, NextResponse } from 'next/server';
// TODO adapt this list to roles with restricted access to some routes + update tests
const authPages = [
'user',
'loyalty-card',
'loyalty-card/*',
'campaigns',
'campaigns/*',
'perks',
Expand Down
2 changes: 0 additions & 2 deletions apps/back-office/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,6 @@ const nextConfig = {
'@nft/event-pass',
],
// https://vercel.com/docs/concepts/deployments/skew-protection#enabling-skew-protection
useDeploymentId: true,
useDeploymentIdServerActions: true,
typedRoutes: false, // no solution found to get it working with nx monorepo (not accessible from external libs like feature)
},
sentry: {
Expand Down
3 changes: 1 addition & 2 deletions apps/back-office/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
"defaultConfiguration": "production",
"options": {
"outputPath": "dist/apps/back-office",
"postcssConfig": "apps/back-office/postcss.config.js",
"debug": true
"postcssConfig": "apps/back-office/postcss.config.js"
},
"assets": [
{
Expand Down
1 change: 1 addition & 0 deletions apps/back-office/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"../../dist/apps/back-office/.next/types/**/*.ts"
],
"exclude": [
"**/{web,unlock}/**",
"**/**/examples.tsx",
"node_modules",
"jest.config.ts",
Expand Down
2 changes: 1 addition & 1 deletion apps/back-office/tsconfig.spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"module": "nodenext",
"types": ["jest", "node", "@testing-library/jest-dom", "@next/types"],
"jsx": "react"
},
Expand Down
3 changes: 2 additions & 1 deletion apps/scripts/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const giveRoleForTest = async (
{
applicantId: `fake-${Math.floor(Math.random() * 10000)}`,
externalUserId: account.id,
createDate: new Date(),
reviewStatus: KycStatus_Enum.Completed,
levelName: KycLevelName_Enum.AdvancedKycLevel,
},
Expand All @@ -52,7 +53,7 @@ export const giveRoleForTest = async (

// Assuming the first argument is the address, the second is the role, and the third is the organizerId
console.log(process.argv);
const address = process.argv[2];
const address = process.argv[2].toLowerCase();
const role = process.argv[3] as Roles_Enum;
const organizerId = process.argv[4];

Expand Down
2 changes: 1 addition & 1 deletion apps/scripts/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "ESNext",
"module": "nodenext",
"types": ["node"]
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
Expand Down
9 changes: 5 additions & 4 deletions apps/scripts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
}
],
"compilerOptions": {
"esModuleInterop": true
}
]
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
OffKeyHeaderConnected,
OffKeyProfile,
OffKeyViewHeaderConnected,
} from '@features/unlock/shopify';
import { messages, type Locale } from '@next/i18n';
import { deepPick } from '@utils';
import { NextIntlClientProvider } from 'next-intl';
import dynamic from 'next/dynamic';

interface HeaderProps {
params: {
Expand All @@ -15,6 +15,11 @@ interface HeaderProps {
};
}

const OffKeyProfile = dynamic(
async () => (await import('@features/unlock/shopify')).OffKeyProfile,
{ ssr: false },
);

export default function Header({
params: { locale, gateId, address },
}: HeaderProps) {
Expand Down
14 changes: 7 additions & 7 deletions apps/unlock/app/[locale]/shopify/[gateId]/[address]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { OffKeyGate, OffKeyState } from '@features/unlock/shopify';
import { messages, type Locale } from '@next/i18n';
import { deepPick } from '@utils';
import { NextIntlClientProvider } from 'next-intl';
import dynamic from 'next/dynamic';

interface GateProps {
params: {
Expand All @@ -11,6 +11,11 @@ interface GateProps {
};
}

const OffKeyGate = dynamic(
async () => (await import('@features/unlock/shopify')).OffKeyGate,
{ ssr: false },
);

export default function Gate({
params: { locale, gateId, address },
}: GateProps) {
Expand All @@ -20,12 +25,7 @@ export default function Gate({
]);
return (
<NextIntlClientProvider locale={locale} messages={localeMessages}>
<OffKeyGate
className="flex-1 pt-2"
gateId={gateId}
address={address}
initialGateState={OffKeyState.Unlocked}
/>
<OffKeyGate className="flex-1 pt-2" gateId={gateId} address={address} />
</NextIntlClientProvider>
);
}
3 changes: 0 additions & 3 deletions apps/unlock/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,6 @@ const nextConfig = {
'@nft/thirdweb-organizer',
'@nft/event-pass',
],
// https://vercel.com/docs/concepts/deployments/skew-protection#enabling-skew-protection
useDeploymentId: true,
useDeploymentIdServerActions: true,
typedRoutes: false, // no solution found to get it working with nx monorepo (not accessible from external libs like feature)
},
sentry: {
Expand Down
3 changes: 1 addition & 2 deletions apps/unlock/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
"defaultConfiguration": "production",
"options": {
"outputPath": "dist/apps/unlock",
"postcssConfig": "apps/unlock/postcss.config.js",
"debug": true
"postcssConfig": "apps/unlock/postcss.config.js"
},
"assets": [
{
Expand Down
1 change: 1 addition & 0 deletions apps/unlock/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"../../dist/apps/unlock/.next/types/**/*.ts"
],
"exclude": [
"**/{web,back-office}/**",
"node_modules",
"**/**/examples.tsx",
"jest.config.ts",
Expand Down
2 changes: 1 addition & 1 deletion apps/unlock/tsconfig.spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"module": "nodenext",
"types": ["jest", "node", "@testing-library/jest-dom", "@next/types"],
"jsx": "react"
},
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/[locale]/cart/purchase/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { isUserKycValidated } from '@kyc/common';
import { redirect } from '@next/navigation';
import { getCurrentUser } from '@next/next-auth/user';
import { redirect as nextRedirect } from 'next/navigation';
import { getErrorMessage } from '@utils';

interface CartSectionProps {
params: {
Expand All @@ -25,7 +26,7 @@ export default async function CartPurchase({
try {
session = await getStripeActiveCheckoutSession();
} catch (error) {
if (error.message === 'User has no email') {
if (getErrorMessage(error) === 'User has no email') {
return redirect('/cart?reason=no-mail');
}
}
Expand Down
43 changes: 43 additions & 0 deletions apps/web/app/api/shopify/admin/_middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import env from '@env/server';
import jwt from 'jsonwebtoken';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';

export const config = {
matcher: '/api/shopify/admin/:path*',
};

// WIP: Complete the implementation of Shopify admin API middleware.
// This includes verifying session tokens, errors and test more thoughtfully with shopify app making requests using APP Bridge
export async function middleware(request: NextRequest) {
const sessionToken = request.headers
.get('authorization')
?.split('Bearer ')[1];
if (!sessionToken) {
return new Response('Unauthorized: No session token provided', {
status: 401,
});
}

try {
await new Promise((resolve, reject) => {
jwt.verify(
sessionToken,
env.SHOPIFY_SHARED_SECRET,
{ algorithms: ['HS256'] },
(err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded);
}
},
);
});

return NextResponse.next();
} catch (error) {
console.error('Token verification error:', error);
return new Response('Unauthorized: Invalid token', { status: 401 });
}
}
21 changes: 21 additions & 0 deletions apps/web/app/api/shopify/loyalty-card/[contractAddress]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ShopifyWebhookAndApiHandler } from '@integrations/external-api-handlers';
import { NextRequest } from 'next/server';

export async function POST(
req: NextRequest,
{ params: { contractAddress } }: { params: { contractAddress: string } },
) {
const shopifyHandler = new ShopifyWebhookAndApiHandler();
return shopifyHandler.mintLoyaltyCardWithCustomerId({
req,
contractAddress,
});
}

export async function GET(
req: NextRequest,
{ params: { contractAddress } }: { params: { contractAddress: string } },
) {
const shopifyHandler = new ShopifyWebhookAndApiHandler();
return shopifyHandler.hasLoyaltyCard({ req, contractAddress });
}
Loading

0 comments on commit 4899d13

Please sign in to comment.